├── tests ├── cases │ ├── simple │ │ ├── b.scss │ │ ├── a.scss │ │ ├── test-config.json │ │ └── main.scss │ ├── css-import │ │ ├── .gitignore │ │ ├── main.scss │ │ ├── test-config.json │ │ └── node_modules │ │ │ └── styles-package │ │ │ └── styles.css │ ├── tilde-import │ │ ├── .gitignore │ │ ├── main.scss │ │ ├── test-config.json │ │ └── node_modules │ │ │ └── styles-package │ │ │ └── styles.scss │ ├── tilde-import-2 │ │ ├── .gitignore │ │ ├── main.scss │ │ ├── test-config.json │ │ └── node_modules │ │ │ └── styles-package │ │ │ ├── another-file.scss │ │ │ └── styles.scss │ ├── whitespace-imports │ │ ├── a.scss │ │ ├── b.scss │ │ ├── test-config.json │ │ └── main.scss │ ├── loop │ │ ├── b.scss │ │ ├── a.scss │ │ ├── test-config.json │ │ └── main.scss │ ├── file-registry │ │ ├── foo.scss │ │ ├── main.scss │ │ ├── test-config.json │ │ └── case.test.tpl │ ├── partial-in-subdir │ │ ├── _partial.scss │ │ ├── test-config.json │ │ ├── subdir │ │ │ ├── another-subdir │ │ │ │ ├── _another-partial2.scss │ │ │ │ └── _another-partial.scss │ │ │ └── _partial2.scss │ │ └── main.scss │ ├── partial │ │ ├── test-config.json │ │ ├── _partial.scss │ │ ├── main.scss │ │ └── subdir │ │ │ └── _partial2.scss │ ├── include-paths │ │ ├── test-config.json │ │ └── main.scss │ ├── partial-error │ │ ├── test-config.json │ │ ├── _partial.scss │ │ ├── subdir │ │ │ └── _partial2.scss │ │ └── main.scss │ ├── comments-project │ │ ├── test-config.json │ │ ├── main.scss │ │ └── additional.scss │ ├── imports-in-comments │ │ ├── test-config.json │ │ └── main.scss │ ├── ignored-imports │ │ ├── node_modules │ │ │ └── @angular │ │ │ │ └── material │ │ │ │ └── theming.scss │ │ ├── main.scss │ │ ├── test-config.json │ │ ├── additional.scss │ │ └── case.test.tpl │ └── __tests__ │ │ └── __snapshots__ │ │ ├── css-import.test.ts.snap │ │ ├── tilde-import.test.ts.snap │ │ ├── file-registry.test.ts.snap │ │ ├── tilde-import-2.test.ts.snap │ │ ├── simple.test.ts.snap │ │ ├── whitespace-imports.test.ts.snap │ │ ├── ignored-imports.test.ts.snap │ │ ├── imports-in-comments.test.ts.snap │ │ ├── partial.test.ts.snap │ │ ├── loop.test.ts.snap │ │ ├── include-paths.test.ts.snap │ │ ├── partial-error.test.ts.snap │ │ ├── partial-in-subdir.test.ts.snap │ │ └── comments-project.test.ts.snap ├── default.test.tpl └── tsconfig.json ├── .eslintrc.json ├── src ├── index.ts ├── cli │ ├── errors │ │ ├── base-error.ts │ │ ├── out-file-not-defined-error.ts │ │ ├── entry-file-not-defined-error.ts │ │ ├── bundle-result-has-no-content-error.ts │ │ ├── compilation-error.ts │ │ ├── entry-file-not-found-error.ts │ │ ├── import-file-not-found-error.ts │ │ └── config-read-error.ts │ ├── constants.ts │ ├── config.ts │ ├── utils │ │ ├── bundle-info.ts │ │ ├── archy.ts │ │ └── scss.ts │ ├── helpers.ts │ ├── logging.ts │ ├── arguments.ts │ └── main.ts ├── helpers.ts ├── contracts.ts └── bundler.ts ├── bundled.scss ├── .prettierrc ├── .editorconfig ├── scss-bundle.config.json ├── .github └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── tsconfig.json ├── .ci ├── pr-build.yml └── build-build.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /tests/cases/simple/b.scss: -------------------------------------------------------------------------------- 1 | $b-variable: blue; -------------------------------------------------------------------------------- /tests/cases/simple/a.scss: -------------------------------------------------------------------------------- 1 | $a-variable: green; -------------------------------------------------------------------------------- /tests/cases/css-import/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /tests/cases/tilde-import/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@reactway" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/tilde-import-2/.gitignore: -------------------------------------------------------------------------------- 1 | !node_modules 2 | -------------------------------------------------------------------------------- /tests/cases/whitespace-imports/a.scss: -------------------------------------------------------------------------------- 1 | $a-variable: green; -------------------------------------------------------------------------------- /tests/cases/whitespace-imports/b.scss: -------------------------------------------------------------------------------- 1 | $b-variable: blue; -------------------------------------------------------------------------------- /tests/cases/loop/b.scss: -------------------------------------------------------------------------------- 1 | @import "a"; 2 | 3 | $b-variable: red; -------------------------------------------------------------------------------- /tests/cases/loop/a.scss: -------------------------------------------------------------------------------- 1 | @import "b"; 2 | 3 | $a-variable: green; -------------------------------------------------------------------------------- /tests/cases/file-registry/foo.scss: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/_partial.scss: -------------------------------------------------------------------------------- 1 | $partial-variable: lightgreen; -------------------------------------------------------------------------------- /tests/cases/tilde-import/main.scss: -------------------------------------------------------------------------------- 1 | @import "~styles-package/styles"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./contracts"; 2 | export * from "./bundler"; 3 | -------------------------------------------------------------------------------- /tests/cases/css-import/main.scss: -------------------------------------------------------------------------------- 1 | @import "~styles-package/styles.css"; 2 | -------------------------------------------------------------------------------- /tests/cases/file-registry/main.scss: -------------------------------------------------------------------------------- 1 | @import "not-exists-in-disk.scss"; 2 | -------------------------------------------------------------------------------- /tests/cases/loop/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/simple/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/tilde-import-2/main.scss: -------------------------------------------------------------------------------- 1 | @import "~styles-package/styles"; 2 | -------------------------------------------------------------------------------- /tests/cases/css-import/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/include-paths/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial-error/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/tilde-import/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/comments-project/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/tilde-import-2/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/whitespace-imports/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/imports-in-comments/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss" 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/css-import/node_modules/styles-package/styles.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial/_partial.scss: -------------------------------------------------------------------------------- 1 | @import "subdir/_partial2"; 2 | $partial-variable: lightgreen; -------------------------------------------------------------------------------- /tests/cases/partial-error/_partial.scss: -------------------------------------------------------------------------------- 1 | @import "subdir/_partial2"; 2 | $partial-variable: lightgreen; -------------------------------------------------------------------------------- /tests/cases/partial-error/subdir/_partial2.scss: -------------------------------------------------------------------------------- 1 | @import "non-existent"; 2 | 3 | $partial-variable: red; -------------------------------------------------------------------------------- /tests/cases/tilde-import/node_modules/styles-package/styles.scss: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/ignored-imports/node_modules/@angular/material/theming.scss: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/subdir/another-subdir/_another-partial2.scss: -------------------------------------------------------------------------------- 1 | $a-super-long-variable-name: red; -------------------------------------------------------------------------------- /tests/cases/partial/main.scss: -------------------------------------------------------------------------------- 1 | @import "partial"; 2 | 3 | .class { 4 | background-color: $partial-variable; 5 | } -------------------------------------------------------------------------------- /tests/cases/partial-error/main.scss: -------------------------------------------------------------------------------- 1 | @import "partial"; 2 | 3 | .class { 4 | background-color: $partial-variable; 5 | } -------------------------------------------------------------------------------- /tests/cases/tilde-import-2/node_modules/styles-package/another-file.scss: -------------------------------------------------------------------------------- 1 | $red: red; 2 | $black: black; 3 | $blue: blue; 4 | -------------------------------------------------------------------------------- /tests/cases/include-paths/main.scss: -------------------------------------------------------------------------------- 1 | @import 'simple/main'; 2 | @import 'node-sass/test/fixtures/custom-functions/setter'; 3 | -------------------------------------------------------------------------------- /tests/cases/partial/subdir/_partial2.scss: -------------------------------------------------------------------------------- 1 | $partial-variable: red; 2 | .partial-class { 3 | color: $partial-variable; 4 | } -------------------------------------------------------------------------------- /tests/cases/loop/main.scss: -------------------------------------------------------------------------------- 1 | @import "a"; 2 | 3 | .class { 4 | background-color: $a-variable; 5 | color: $b-variable; 6 | } -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/subdir/another-subdir/_another-partial.scss: -------------------------------------------------------------------------------- 1 | @import "./another-partial2"; 2 | 3 | $another-variable: red; -------------------------------------------------------------------------------- /tests/cases/tilde-import-2/node_modules/styles-package/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./another-file"; 2 | 3 | .foo { 4 | color: $red; 5 | } 6 | -------------------------------------------------------------------------------- /bundled.scss: -------------------------------------------------------------------------------- 1 | $a-variable: green; 2 | $b-variable: blue; 3 | 4 | .class { 5 | background-color: $a-variable; 6 | color: $b-variable; 7 | } -------------------------------------------------------------------------------- /tests/cases/ignored-imports/main.scss: -------------------------------------------------------------------------------- 1 | @import "./additional"; 2 | @import '~@angular/material/theming'; 3 | 4 | html { 5 | color: black; 6 | } 7 | -------------------------------------------------------------------------------- /tests/cases/ignored-imports/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss", 3 | "ignoredImports": [ 4 | "~@angular/.*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": false, 6 | "printWidth": 140 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/errors/base-error.ts: -------------------------------------------------------------------------------- 1 | export class BaseError extends Error { 2 | public toString(): string { 3 | return this.message; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/cases/ignored-imports/additional.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | @include mat-core(); 4 | 5 | .test { 6 | color: red; 7 | } 8 | -------------------------------------------------------------------------------- /tests/cases/simple/main.scss: -------------------------------------------------------------------------------- 1 | @import "./a.scss"; 2 | @import "b"; 3 | 4 | .class { 5 | background-color: $a-variable; 6 | color: $b-variable; 7 | } -------------------------------------------------------------------------------- /tests/cases/whitespace-imports/main.scss: -------------------------------------------------------------------------------- 1 | @import "./a.scss"; 2 | @import "b"; 3 | 4 | .class { 5 | background-color: $a-variable; 6 | color: $b-variable; 7 | } -------------------------------------------------------------------------------- /tests/cases/comments-project/main.scss: -------------------------------------------------------------------------------- 1 | // Simple project with LF as line endings. 2 | //@import "./additional"; 3 | @import "./additional"; 4 | 5 | html { 6 | color: black; 7 | } 8 | -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/subdir/_partial2.scss: -------------------------------------------------------------------------------- 1 | @import "another-subdir/another-partial"; 2 | 3 | $partial-variable: red; 4 | 5 | .partial-class { 6 | color: $partial-variable; 7 | } -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/css-import.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`css-import 1`] = ` 4 | ".foo { 5 | color: red; 6 | } 7 | 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/tilde-import.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`tilde-import 1`] = ` 4 | ".foo { 5 | color: red; 6 | } 7 | 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /tests/cases/partial-in-subdir/main.scss: -------------------------------------------------------------------------------- 1 | @import "partial"; 2 | @import "subdir/another-subdir/another-partial"; 3 | @import "subdir/partial2"; 4 | 5 | .class { 6 | background-color: $partial-variable; 7 | } -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/file-registry.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`file-registry 1`] = ` 4 | ".foo { 5 | color: red; 6 | } 7 | 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /tests/cases/file-registry/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "main.scss", 3 | "registries": [ 4 | { 5 | "path": "not-exists-in-disk.scss", 6 | "content": "@import \"foo\";" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/errors/out-file-not-defined-error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from "./base-error"; 2 | 3 | export class OutFileNotDefinedError extends BaseError { 4 | constructor() { 5 | super(`"outFile" is not defined.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [{src, tests}/**.{ts,json,js}] 8 | charset = utf-8 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /src/cli/errors/entry-file-not-defined-error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from "./base-error"; 2 | 3 | export class EntryFileNotDefinedError extends BaseError { 4 | constructor() { 5 | super(`"entryFile" is not defined.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/cli/constants.ts: -------------------------------------------------------------------------------- 1 | export const CONFIG_FILE_NAME = "scss-bundle.config.json" as const; 2 | 3 | export enum LogLevel { 4 | Trace = 0, 5 | Debug = 1, 6 | Info = 2, 7 | Warning = 3, 8 | Error = 4, 9 | Silent = 5 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/errors/bundle-result-has-no-content-error.ts: -------------------------------------------------------------------------------- 1 | import { BaseError } from "./base-error"; 2 | 3 | export class BundleResultHasNoContentError extends BaseError { 4 | constructor() { 5 | super("Concatenation result has no content."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/cases/imports-in-comments/main.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Hello this is the comment. 3 | @import "css-framework"; 4 | */ 5 | 6 | // This is one line comment @import "css-framework"; 7 | 8 | //@import "css-framework"; 9 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/tilde-import-2.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`tilde-import-2 1`] = ` 4 | "$red: red; 5 | $black: black; 6 | $blue: blue; 7 | 8 | 9 | .foo { 10 | color: $red; 11 | } 12 | 13 | " 14 | `; 15 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/simple.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`simple 1`] = ` 4 | "$a-variable: green; 5 | $b-variable: blue; 6 | 7 | .class { 8 | background-color: $a-variable; 9 | color: $b-variable; 10 | }" 11 | `; 12 | -------------------------------------------------------------------------------- /src/cli/errors/compilation-error.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import { BaseError } from "./base-error"; 3 | 4 | export class CompilationError extends BaseError { 5 | constructor(styleError: string) { 6 | super(`There is an error in your styles:${os.EOL}${styleError}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cli/errors/entry-file-not-found-error.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import { BaseError } from "./base-error"; 3 | 4 | export class EntryFileNotFoundError extends BaseError { 5 | constructor(filePath: string) { 6 | super(`Entry file was not found:${os.EOL}${filePath}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cli/errors/import-file-not-found-error.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import { BaseError } from "./base-error"; 3 | 4 | export class ImportFileNotFoundError extends BaseError { 5 | constructor(filePath: string) { 6 | super(`Import file was not found:${os.EOL}${filePath}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scss-bundle.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundlerOptions": { 3 | "entryFile": "./tests/cases/simple/main.scss", 4 | "rootDir": "./tests/cases/simple/", 5 | "outFile": "./bundled.scss", 6 | "ignoreImports": ["~@angular/.*"], 7 | "logLevel": "silent" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/errors/config-read-error.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import { BaseError } from "./base-error"; 3 | 4 | export class ConfigReadError extends BaseError { 5 | constructor(configPath: string) { 6 | super(`Failed to read config (maybe it's missing?):${os.EOL}${configPath}`); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/whitespace-imports.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`whitespace-imports 1`] = ` 4 | "$a-variable: green; 5 | $b-variable: blue; 6 | 7 | .class { 8 | background-color: $a-variable; 9 | color: $b-variable; 10 | }" 11 | `; 12 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function matchAll(text: string, regex: RegExp): RegExpExecArray[] { 2 | const matches: RegExpExecArray[] = []; 3 | 4 | let match: RegExpExecArray | null; 5 | while ((match = regex.exec(text))) { 6 | matches.push(match); 7 | } 8 | 9 | return matches; 10 | } 11 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/ignored-imports.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ignored-imports 1`] = ` 4 | "@import '~@angular/material/theming'; 5 | 6 | @include mat-core(); 7 | 8 | .test { 9 | color: red; 10 | } 11 | 12 | 13 | 14 | html { 15 | color: black; 16 | } 17 | " 18 | `; 19 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/imports-in-comments.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`imports-in-comments 1`] = ` 4 | "/* 5 | Hello this is the comment. 6 | 7 | */ 8 | 9 | // This is one line comment 10 | 11 | // 12 | " 13 | `; 14 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/partial.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`partial 1`] = ` 4 | "$partial-variable: red; 5 | .partial-class { 6 | color: $partial-variable; 7 | } 8 | $partial-variable: lightgreen; 9 | 10 | .class { 11 | background-color: $partial-variable; 12 | }" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/loop.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`loop 1`] = ` 4 | "@import \\"b\\"; 5 | 6 | $a-variable: green; 7 | 8 | $b-variable: red; 9 | 10 | $a-variable: green; 11 | 12 | .class { 13 | background-color: $a-variable; 14 | color: $b-variable; 15 | }" 16 | `; 17 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/include-paths.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`include-paths 1`] = ` 4 | "/*** IMPORTED FILE NOT FOUND ***/ 5 | @import 'simple/main';/*** --- ***/ 6 | /*** IMPORTED FILE NOT FOUND ***/ 7 | @import 'node-sass/test/fixtures/custom-functions/setter';/*** --- ***/ 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/partial-error.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`partial-error 1`] = ` 4 | "/*** IMPORTED FILE NOT FOUND ***/ 5 | @import \\"non-existent\\";/*** --- ***/ 6 | 7 | $partial-variable: red; 8 | $partial-variable: lightgreen; 9 | 10 | .class { 11 | background-color: $partial-variable; 12 | }" 13 | `; 14 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/partial-in-subdir.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`partial-in-subdir 1`] = ` 4 | "$partial-variable: lightgreen; 5 | $a-super-long-variable-name: red; 6 | 7 | $another-variable: red; 8 | $a-super-long-variable-name: red; 9 | 10 | $another-variable: red; 11 | 12 | $partial-variable: red; 13 | 14 | .partial-class { 15 | color: $partial-variable; 16 | } 17 | 18 | .class { 19 | background-color: $partial-variable; 20 | }" 21 | `; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Package version:** vX.X.X 21 | **Node version:** vX.X.X 22 | **OS:** Windows/Linux/Mac OS 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "jsx": "react", 8 | "noUnusedLocals": true, 9 | "declaration": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmitHelpers": true, 13 | "importHelpers": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "newLine": "LF" 17 | }, 18 | "exclude": ["**/__tests__", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /tests/default.test.tpl: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Bundler } from "@src/bundler"; 3 | 4 | test("{{caseName}}", async done => { 5 | const projectDirectory = "{{projectDirectory}}"; 6 | const testConfig = {{{json testConfig}}}; 7 | const entryFile = path.join(projectDirectory, testConfig.entry); 8 | 9 | try { 10 | const bundleResult = await new Bundler(undefined, projectDirectory) 11 | .bundle(entryFile); 12 | 13 | expect(bundleResult.bundledContent).toMatchSnapshot(); 14 | done(); 15 | } catch (error) { 16 | done.fail(error); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "removeComments": false, 6 | "sourceMap": false, 7 | "skipDefaultLibCheck": true, 8 | "pretty": true, 9 | "noEmit": true, 10 | "experimentalDecorators": false, 11 | "baseUrl": "./", 12 | "types": ["jest", "node"], 13 | "lib": ["es6", "dom"], 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "paths": { 17 | "@src/*": ["../src/*"] 18 | }, 19 | "rootDirs": ["./", "../src"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/cases/ignored-imports/case.test.tpl: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { Bundler } from "@src/bundler"; 3 | 4 | test("{{caseName}}", async done => { 5 | const projectDirectory = "{{projectDirectory}}"; 6 | const testConfig = {{{json testConfig}}}; 7 | const entryFile = path.join(projectDirectory, testConfig.entry); 8 | 9 | try { 10 | const bundleResult = await new Bundler(undefined, projectDirectory) 11 | .bundle(entryFile, [], [], testConfig.ignoredImports) 12 | 13 | expect(bundleResult.bundledContent).toMatchSnapshot(); 14 | done(); 15 | } catch (error) { 16 | done.fail(error); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /tests/cases/file-registry/case.test.tpl: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Bundler } from "@src/bundler"; 3 | 4 | test("{{caseName}}", async done => { 5 | const projectDirectory = "{{projectDirectory}}"; 6 | const testConfig = {{{json testConfig}}}; 7 | const entryFile = path.join(projectDirectory, testConfig.entry); 8 | 9 | const fileRegistry = Object.assign( 10 | {}, 11 | ...testConfig.registries.map((registry) => ({ 12 | [path.join(projectDirectory, registry.path)]: registry.content 13 | })) 14 | ); 15 | 16 | try { 17 | const bundleResult = await new Bundler(fileRegistry, projectDirectory) 18 | .bundle(entryFile); 19 | 20 | expect(bundleResult.bundledContent).toMatchSnapshot(); 21 | done(); 22 | } catch (error) { 23 | done.fail(error); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/contracts.ts: -------------------------------------------------------------------------------- 1 | export interface ScssBundleConfig { 2 | bundlerOptions: BundlerOptions; 3 | } 4 | 5 | export interface BundlerOptions { 6 | project?: string; 7 | entryFile?: string; 8 | outFile?: string; 9 | rootDir?: string; 10 | ignoreImports?: string[]; 11 | includePaths?: string[]; 12 | dedupeGlobs?: string[]; 13 | watch?: boolean; 14 | logLevel?: string; 15 | } 16 | 17 | export interface FileRegistry { 18 | [id: string]: string | undefined; 19 | } 20 | 21 | export interface ImportData { 22 | importString: string; 23 | tilde: boolean; 24 | path: string; 25 | fullPath: string; 26 | found: boolean; 27 | ignored?: boolean; 28 | } 29 | 30 | export interface BundleResult { 31 | // Child imports (if any) 32 | imports?: BundleResult[]; 33 | tilde?: boolean; 34 | deduped?: boolean; 35 | // Full path of the file 36 | filePath: string; 37 | bundledContent?: string; 38 | found: boolean; 39 | ignored?: boolean; 40 | } 41 | -------------------------------------------------------------------------------- /.ci/pr-build.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | pool: 5 | vmImage: "ubuntu-latest" 6 | 7 | steps: 8 | - task: NodeTool@0 9 | inputs: 10 | versionSpec: "12.x" 11 | displayName: "Install Node.js" 12 | 13 | - script: | 14 | npm install 15 | displayName: "npm install" 16 | 17 | - script: | 18 | npm run build 19 | displayName: "npm run build" 20 | 21 | - script: | 22 | npm test 23 | displayName: "npm test" 24 | 25 | - task: PublishTestResults@2 26 | displayName: "Publish Test Results junit.xml" 27 | inputs: 28 | testResultsFiles: junit.xml 29 | failTaskOnFailedTests: true 30 | 31 | - task: PublishCodeCoverageResults@1 32 | displayName: "Publish code coverage from $(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml" 33 | inputs: 34 | codeCoverageTool: Cobertura 35 | summaryFileLocation: "$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml" 36 | -------------------------------------------------------------------------------- /src/cli/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import { ScssBundleConfig } from "../contracts"; 3 | import { resolvePath } from "./helpers"; 4 | import { ConfigReadError } from "./errors/config-read-error"; 5 | 6 | export async function resolveConfig(filePath: string): Promise { 7 | let json: ScssBundleConfig; 8 | try { 9 | json = await fs.readJson(filePath); 10 | } catch { 11 | throw new ConfigReadError(filePath); 12 | } 13 | 14 | if (json.bundlerOptions == null) { 15 | throw new Error("Missing 'bundlerOptions' in config."); 16 | } 17 | 18 | return { 19 | bundlerOptions: { 20 | ...json.bundlerOptions, 21 | entryFile: json.bundlerOptions.entryFile != null ? resolvePath(json.bundlerOptions.entryFile) : undefined, 22 | outFile: json.bundlerOptions.outFile != null ? resolvePath(json.bundlerOptions.outFile) : undefined, 23 | rootDir: json.bundlerOptions.rootDir != null ? resolvePath(json.bundlerOptions.rootDir) : undefined 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 QuatroDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/cli/utils/bundle-info.ts: -------------------------------------------------------------------------------- 1 | import os from "os"; 2 | import prettyBytes from "pretty-bytes"; 3 | 4 | import { BundleResult, FileRegistry } from "../../contracts"; 5 | 6 | function countSavedBytesByDeduping(bundleResult: BundleResult, fileRegistry: FileRegistry): number { 7 | let savedBytes = 0; 8 | const content = fileRegistry[bundleResult.filePath]; 9 | if (bundleResult.deduped === true && content != null) { 10 | savedBytes = content.length; 11 | } 12 | if (bundleResult.imports != null && bundleResult.imports.length > 0) { 13 | for (const importResult of bundleResult.imports) { 14 | savedBytes += countSavedBytesByDeduping(importResult, fileRegistry); 15 | } 16 | } 17 | return savedBytes; 18 | } 19 | 20 | export function renderBundleInfo(bundleResult: BundleResult, fileRegistry: FileRegistry): string { 21 | return [ 22 | "Bundle info:", 23 | `Total size : ${bundleResult.bundledContent == null ? "undefined" : prettyBytes(bundleResult.bundledContent.length)}`, 24 | `Saved by deduping: ${prettyBytes(countSavedBytesByDeduping(bundleResult, fileRegistry))}` 25 | ].join(os.EOL); 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/utils/archy.ts: -------------------------------------------------------------------------------- 1 | import archy from "archy"; 2 | import path from "path"; 3 | 4 | import { BundleResult } from "../../contracts"; 5 | 6 | function getArchyData(bundleResult: BundleResult, sourceDirectory?: string): archy.Data { 7 | if (sourceDirectory == null) { 8 | sourceDirectory = process.cwd(); 9 | } 10 | const archyData: archy.Data = { 11 | label: path.relative(sourceDirectory, bundleResult.filePath) 12 | }; 13 | 14 | if (!bundleResult.found) { 15 | archyData.label += ` [NOT FOUND]`; 16 | } 17 | if (bundleResult.deduped) { 18 | archyData.label += ` [DEDUPED]`; 19 | } 20 | if (bundleResult.ignored) { 21 | archyData.label += ` [IGNORED]`; 22 | } 23 | 24 | if (bundleResult.imports != null) { 25 | archyData.nodes = bundleResult.imports.map(x => { 26 | if (x != null) { 27 | return getArchyData(x, sourceDirectory); 28 | } 29 | return ""; 30 | }); 31 | } 32 | return archyData; 33 | } 34 | 35 | export function renderArchy(bundleResult: BundleResult, sourceDirectory?: string): string { 36 | return archy(getArchyData(bundleResult, sourceDirectory)); 37 | } 38 | -------------------------------------------------------------------------------- /src/cli/utils/scss.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import nodeSass from "sass"; 3 | import { CompilationError } from "../errors/compilation-error"; 4 | 5 | function sassImporter(projectPath: string): nodeSass.Importer { 6 | return (url, _prev, done) => { 7 | if (url[0] === "~") { 8 | const filePath = path.resolve(projectPath, "node_modules", url.substr(1)); 9 | done({ 10 | file: filePath 11 | }); 12 | } else { 13 | done({ file: url }); 14 | } 15 | }; 16 | } 17 | 18 | export async function renderScss(projectPath: string | undefined, includePaths: string[] | undefined, content: string): Promise<{}> { 19 | return new Promise((resolve, reject) => { 20 | nodeSass.render( 21 | { 22 | data: content, 23 | importer: projectPath != null ? sassImporter(projectPath) : undefined, 24 | includePaths: includePaths 25 | }, 26 | (error, result) => { 27 | if (error != null) { 28 | reject(new CompilationError(`${error.message} on line (${error.line}, ${error.column})`)); 29 | } 30 | resolve(result); 31 | } 32 | ); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/cli/helpers.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { LogLevel } from "./constants"; 3 | 4 | export function resolveBoolean(value: string): boolean | undefined { 5 | if (value === "true") { 6 | return true; 7 | } else if (value === "false") { 8 | return false; 9 | } 10 | 11 | return undefined; 12 | } 13 | 14 | export function resolveList(value: string): string[] | undefined { 15 | return value.split(","); 16 | } 17 | 18 | export function resolvePath(value: string): string { 19 | return path.resolve(path.normalize(value)); 20 | } 21 | 22 | export function resolveLogLevelKey(value: string): string | undefined { 23 | return Object.keys(LogLevel).find(x => x.toLowerCase() === value.toLowerCase()); 24 | } 25 | 26 | export function mergeObjects(a: TAObject, b: TBObject): TAObject & TBObject { 27 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 28 | const result: { [key: string]: unknown } = a as any; 29 | 30 | for (const key of Object.keys(b)) { 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | const value = (b as any)[key]; 33 | if (value == null) { 34 | continue; 35 | } 36 | 37 | result[key] = value; 38 | } 39 | 40 | return result as TAObject & TBObject; 41 | } 42 | -------------------------------------------------------------------------------- /src/cli/logging.ts: -------------------------------------------------------------------------------- 1 | import chalk, { Chalk } from "chalk"; 2 | import log from "loglevel"; 3 | import prefix from "loglevel-plugin-prefix"; 4 | 5 | const colors: { [key: string]: Chalk } = { 6 | trace: chalk.white, 7 | debug: chalk.white, 8 | info: chalk.green, 9 | warn: chalk.yellow, 10 | error: chalk.red 11 | }; 12 | 13 | const levels: { [key: string]: string } = { 14 | trace: "trce", 15 | debug: "dbug", 16 | info: "info", 17 | warn: "warn", 18 | error: "erro" 19 | }; 20 | 21 | prefix.reg(log); 22 | log.enableAll(); 23 | 24 | prefix.apply(log, { 25 | format(level, _, timestamp) { 26 | return `${chalk.gray(`[${timestamp}]`)} ${colors[level.toLowerCase()](`${levels[level.toLowerCase()]}:`)}`; 27 | } 28 | }); 29 | 30 | function appyMultilineText(logger: log.RootLogger): log.RootLogger { 31 | const originalFactory = logger.methodFactory; 32 | 33 | logger.methodFactory = (methodName, logLevel, loggerName) => { 34 | const rawMethod = originalFactory(methodName, logLevel, loggerName); 35 | return (message: unknown) => { 36 | String(message) 37 | .split("\n") 38 | .forEach(x => rawMethod(x)); 39 | }; 40 | }; 41 | 42 | logger.setLevel(logger.getLevel()); 43 | return logger; 44 | } 45 | 46 | export const Log = appyMultilineText(log); 47 | -------------------------------------------------------------------------------- /src/cli/arguments.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import { resolveBoolean, resolveList, resolvePath, resolveLogLevelKey } from "./helpers"; 3 | import { BundlerOptions } from "../contracts"; 4 | 5 | export interface Arguments extends BundlerOptions { 6 | config?: string; 7 | } 8 | 9 | export function resolveArguments(cmd: commander.Command, argv: string[]): Arguments { 10 | const parsedArguments = (cmd 11 | .option("-c, --config ", "configuration file location", resolvePath) 12 | .option("-p, --project ", "project location where 'node_modules' folder is located", resolvePath) 13 | .option("-e, --entryFile ", "bundle entry file location", resolvePath) 14 | .option("-o, --outFile ", "bundle output location", resolvePath) 15 | .option("--rootDir ", "specifies the root directory of input files", resolvePath) 16 | .option("-w, --watch [boolean]", `watch files for changes. Works with "rootDir"`, resolveBoolean) 17 | .option("--ignoreImports ", "ignore resolving import content by matching a regular expression", resolveList) 18 | .option("--includePaths ", "include paths for resolving imports", resolveList) 19 | .option("--dedupeGlobs ", "files that will be emitted in a bundle once", resolveList) 20 | .option("--logLevel ", "console log level", resolveLogLevelKey) 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | .parse(argv) as any) as Arguments; 23 | 24 | const { config, project, entryFile, ignoreImports, includePaths, outFile, rootDir, watch, logLevel, dedupeGlobs } = parsedArguments; 25 | 26 | return { 27 | config, 28 | project, 29 | entryFile, 30 | ignoreImports, 31 | includePaths, 32 | outFile, 33 | rootDir, 34 | watch, 35 | logLevel, 36 | dedupeGlobs 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /.ci/build-build.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | pool: 5 | vmImage: "ubuntu-latest" 6 | 7 | steps: 8 | - task: NodeTool@0 9 | inputs: 10 | versionSpec: "12.x" 11 | displayName: "Install Node.js" 12 | 13 | - script: | 14 | npm install 15 | displayName: "npm install" 16 | 17 | - script: | 18 | npm run build 19 | displayName: "npm run build" 20 | 21 | - script: | 22 | npm test 23 | displayName: "npm test" 24 | 25 | - task: PublishTestResults@2 26 | displayName: "Publish Test Results junit.xml" 27 | inputs: 28 | testResultsFiles: junit.xml 29 | failTaskOnFailedTests: true 30 | 31 | - task: PublishCodeCoverageResults@1 32 | displayName: "Publish code coverage from $(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml" 33 | inputs: 34 | codeCoverageTool: Cobertura 35 | summaryFileLocation: "$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml" 36 | 37 | - script: | 38 | npm version 0.0.0-canary.$(git rev-parse --short HEAD) --no-git-tag-version 39 | displayName: "Apply cannary version" 40 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev')) 41 | 42 | - script: | 43 | npm pack 44 | displayName: "npm pack" 45 | 46 | - script: | 47 | mkdir $(Build.ArtifactStagingDirectory)/packages 48 | mv *.tgz $(Build.ArtifactStagingDirectory)/packages 49 | displayName: "Move tgz to artifacts folder" 50 | 51 | - task: PublishBuildArtifacts@1 52 | displayName: "Publish Artifact: packages" 53 | inputs: 54 | PathtoPublish: "$(Build.ArtifactStagingDirectory)/packages" 55 | ArtifactName: packages 56 | 57 | - task: PublishBuildArtifacts@1 58 | displayName: "Publish Artifact: package.json" 59 | inputs: 60 | PathtoPublish: "$(System.DefaultWorkingDirectory)/package.json" 61 | ArtifactName: packageJson 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scss-bundle", 3 | "version": "3.1.2", 4 | "description": "Bundling SCSS files to one bundled file.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p .", 9 | "watch": "tsc -p . -w", 10 | "start": "node dist/cli/main.js", 11 | "pretest": "tsc -p . --noEmit && eslint \"src/**/*.ts*\"", 12 | "test": "npm run build-tests && jest", 13 | "build-tests": "test-generator-cli" 14 | }, 15 | "keywords": [ 16 | "scss", 17 | "bundle", 18 | "sass", 19 | "node-sass" 20 | ], 21 | "files": [ 22 | "dist", 23 | "docs", 24 | "*.js", 25 | "!*.config.js" 26 | ], 27 | "bin": { 28 | "scss-bundle": "dist/cli/main.js" 29 | }, 30 | "author": "ReactWay (https://github.com/reactway)", 31 | "bugs": "https://github.com/reactway/scss-bundle/issues", 32 | "repository": "reactway/scss-bundle", 33 | "homepage": "https://github.com/reactway/scss-bundle", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@types/jest": "^24.0.23", 37 | "jest": "^24.9.0", 38 | "ts-jest": "^24.2.0", 39 | "@reactway/eslint-config": "^1.0.0-alpha.2", 40 | "eslint": "^6.7.2", 41 | "typescript": "^3.7.3", 42 | "@simplrjs/test-generator-cli": "^0.1.3", 43 | "jest-junit": "^10.0.0" 44 | }, 45 | "dependencies": { 46 | "@types/archy": "^0.0.31", 47 | "@types/debug": "^4.1.5", 48 | "@types/fs-extra": "^8.0.1", 49 | "@types/glob": "^7.1.1", 50 | "@types/lodash.debounce": "^4.0.6", 51 | "@types/sass": "^1.16.0", 52 | "archy": "^1.0.0", 53 | "chalk": "^3.0.0", 54 | "chokidar": "^3.3.1", 55 | "commander": "^4.0.1", 56 | "fs-extra": "^8.1.0", 57 | "globs": "^0.1.4", 58 | "lodash.debounce": "^4.0.8", 59 | "loglevel": "^1.6.6", 60 | "loglevel-plugin-prefix": "^0.8.4", 61 | "pretty-bytes": "^5.3.0", 62 | "sass": "^1.23.7", 63 | "tslib": "^1.10.0" 64 | }, 65 | "jest": { 66 | "verbose": true, 67 | "preset": "ts-jest", 68 | "reporters": [ 69 | "default", 70 | "jest-junit" 71 | ], 72 | "collectCoverage": true, 73 | "testRegex": "/__tests__/.*\\.(test|spec).(ts|tsx)$", 74 | "moduleNameMapper": { 75 | "@src/(.*)": "/src/$1" 76 | }, 77 | "collectCoverageFrom": [ 78 | "src/**/*.{ts,tsx}", 79 | "!src/**/__tests__/*", 80 | "!src/index.ts", 81 | "!src/cli/**/*" 82 | ], 83 | "coverageReporters": [ 84 | "cobertura" 85 | ], 86 | "globals": { 87 | "ts-jest": { 88 | "tsConfig": "./tests/tsconfig.json" 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/cases/comments-project/additional.scss: -------------------------------------------------------------------------------- 1 | // stacking context for all overlays. 2 | $cdk-z-index-overlay-container: 1000 !default; 3 | $cdk-z-index-overlay: 1000 !default; 4 | $cdk-z-index-overlay-backdrop: 1000 !default; 5 | 6 | // Background color for all of the backdrops 7 | $cdk-overlay-dark-backdrop-background: rgba(0, 0, 0, 0.6) !default; 8 | 9 | //Default backdrop animation is based on the Material Design swift-ease-out. 10 | $backdrop-animation-duration: 400ms !default; 11 | $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; 12 | 13 | // Imported from Angular Material 14 | @mixin cdk-overlay() { 15 | .cdk-overlay-container, .cdk-global-overlay-wrapper { 16 | // Disable events from being captured on the overlay container. 17 | pointer-events: none; 18 | 19 | // The container should be the size of the viewport. 20 | top: 0; 21 | left: 0; 22 | height: 100%; 23 | width: 100%; 24 | } 25 | 26 | // The overlay-container is an invisible element which contains all individual overlays. 27 | .cdk-overlay-container { 28 | position: fixed; 29 | z-index: $cdk-z-index-overlay-container; 30 | } 31 | 32 | // We use an extra wrapper element in order to use make the overlay itself a flex item. 33 | // This makes centering the overlay easy without running into the subpixel rendering 34 | // problems tied to using `transform` and without interfering with the other position 35 | // strategies. 36 | .cdk-global-overlay-wrapper { 37 | display: flex; 38 | position: absolute; 39 | z-index: $cdk-z-index-overlay; 40 | } 41 | 42 | // A single overlay pane. 43 | .cdk-overlay-pane { 44 | position: absolute; 45 | pointer-events: auto; 46 | box-sizing: border-box; 47 | z-index: $cdk-z-index-overlay; 48 | } 49 | 50 | .cdk-overlay-backdrop { 51 | position: absolute; 52 | top: 0; 53 | bottom: 0; 54 | left: 0; 55 | right: 0; 56 | 57 | z-index: $cdk-z-index-overlay-backdrop; 58 | pointer-events: auto; 59 | -webkit-tap-highlight-color: transparent; 60 | transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function; 61 | opacity: 0; 62 | 63 | &.cdk-overlay-backdrop-showing { 64 | opacity: 0.48; 65 | } 66 | } 67 | 68 | .cdk-overlay-dark-backdrop { 69 | background: $cdk-overlay-dark-backdrop-background; 70 | } 71 | 72 | .cdk-overlay-transparent-backdrop { 73 | background: none; 74 | } 75 | 76 | // Used when disabling global scrolling. 77 | .cdk-global-scrollblock { 78 | position: fixed; 79 | 80 | // Necessary for the content not to lose its width. Note that we're using 100%, instead of 81 | // 100vw, because 100vw includes the width plus the scrollbar, whereas 100% is the width 82 | // that the element had before we made it `fixed`. 83 | width: 100%; 84 | 85 | // Note: this will always add a scrollbar to whatever element it is on, which can 86 | // potentially result in double scrollbars. It shouldn't be an issue, because we won't 87 | // block scrolling on a page that doesn't have a scrollbar in the first place. */ 88 | overflow-y: scroll; 89 | } 90 | } 91 | 92 | @include cdk-overlay(); 93 | -------------------------------------------------------------------------------- /tests/cases/__tests__/__snapshots__/comments-project.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`comments-project 1`] = ` 4 | "// Simple project with LF as line endings. 5 | // 6 | // stacking context for all overlays. 7 | $cdk-z-index-overlay-container: 1000 !default; 8 | $cdk-z-index-overlay: 1000 !default; 9 | $cdk-z-index-overlay-backdrop: 1000 !default; 10 | 11 | // Background color for all of the backdrops 12 | $cdk-overlay-dark-backdrop-background: rgba(0, 0, 0, 0.6) !default; 13 | 14 | //Default backdrop animation is based on the Material Design swift-ease-out. 15 | $backdrop-animation-duration: 400ms !default; 16 | $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; 17 | 18 | // Imported from Angular Material 19 | @mixin cdk-overlay() { 20 | .cdk-overlay-container, .cdk-global-overlay-wrapper { 21 | // Disable events from being captured on the overlay container. 22 | pointer-events: none; 23 | 24 | // The container should be the size of the viewport. 25 | top: 0; 26 | left: 0; 27 | height: 100%; 28 | width: 100%; 29 | } 30 | 31 | // The overlay-container is an invisible element which contains all individual overlays. 32 | .cdk-overlay-container { 33 | position: fixed; 34 | z-index: $cdk-z-index-overlay-container; 35 | } 36 | 37 | // We use an extra wrapper element in order to use make the overlay itself a flex item. 38 | // This makes centering the overlay easy without running into the subpixel rendering 39 | // problems tied to using \`transform\` and without interfering with the other position 40 | // strategies. 41 | .cdk-global-overlay-wrapper { 42 | display: flex; 43 | position: absolute; 44 | z-index: $cdk-z-index-overlay; 45 | } 46 | 47 | // A single overlay pane. 48 | .cdk-overlay-pane { 49 | position: absolute; 50 | pointer-events: auto; 51 | box-sizing: border-box; 52 | z-index: $cdk-z-index-overlay; 53 | } 54 | 55 | .cdk-overlay-backdrop { 56 | position: absolute; 57 | top: 0; 58 | bottom: 0; 59 | left: 0; 60 | right: 0; 61 | 62 | z-index: $cdk-z-index-overlay-backdrop; 63 | pointer-events: auto; 64 | -webkit-tap-highlight-color: transparent; 65 | transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function; 66 | opacity: 0; 67 | 68 | &.cdk-overlay-backdrop-showing { 69 | opacity: 0.48; 70 | } 71 | } 72 | 73 | .cdk-overlay-dark-backdrop { 74 | background: $cdk-overlay-dark-backdrop-background; 75 | } 76 | 77 | .cdk-overlay-transparent-backdrop { 78 | background: none; 79 | } 80 | 81 | // Used when disabling global scrolling. 82 | .cdk-global-scrollblock { 83 | position: fixed; 84 | 85 | // Necessary for the content not to lose its width. Note that we're using 100%, instead of 86 | // 100vw, because 100vw includes the width plus the scrollbar, whereas 100% is the width 87 | // that the element had before we made it \`fixed\`. 88 | width: 100%; 89 | 90 | // Note: this will always add a scrollbar to whatever element it is on, which can 91 | // potentially result in double scrollbars. It shouldn't be an issue, because we won't 92 | // block scrolling on a page that doesn't have a scrollbar in the first place. */ 93 | overflow-y: scroll; 94 | } 95 | } 96 | 97 | @include cdk-overlay(); 98 | 99 | 100 | html { 101 | color: black; 102 | } 103 | " 104 | `; 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | #**/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/* 194 | !tests/cases/*/node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | .paket/paket.exe 246 | paket-files/ 247 | 248 | # FAKE - F# Make 249 | .fake/ 250 | 251 | # JetBrains Rider 252 | .idea/ 253 | *.sln.iml 254 | yarn.lock 255 | 256 | packages/react-forms-test 257 | 258 | dist 259 | @types 260 | 261 | # MacOS 262 | .DS_Store 263 | 264 | # Ignore Rush temporary files 265 | /common/temp/** 266 | 267 | **/webpack.config.js 268 | 269 | **/*.js.map 270 | 271 | _src 272 | coverage 273 | 274 | # Rush files 275 | **/*.build.error.log 276 | **/*.build.log 277 | /common/apiDocs/json/** 278 | /common/last-install.flag 279 | /common/last-install.log 280 | /common/local-npm 281 | /common/local-npm/** 282 | /common/local-rush 283 | /common/local-rush/** 284 | /common/npm-cache 285 | /common/npm-cache/** 286 | /common/npm-local 287 | /common/npm-local/** 288 | /common/npm-tmp 289 | /common/npm-tmp/** 290 | /common/rush-link.json 291 | /common/rush-recycler 292 | package-deps.json 293 | 294 | __temp__ 295 | 296 | # Tests 297 | **/tests/cases/__tests__/*.test.ts 298 | junit.xml 299 | -------------------------------------------------------------------------------- /src/cli/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import commander from "commander"; 4 | import fs from "fs-extra"; 5 | import path from "path"; 6 | import debounce from "lodash.debounce"; 7 | import chokidar from "chokidar"; 8 | 9 | import { BundlerOptions, FileRegistry, BundleResult } from "../contracts"; 10 | import { resolveArguments } from "./arguments"; 11 | import { CONFIG_FILE_NAME, LogLevel } from "./constants"; 12 | import { resolveConfig } from "./config"; 13 | import { Log } from "./logging"; 14 | import { Bundler } from "../bundler"; 15 | import { EntryFileNotFoundError } from "./errors/entry-file-not-found-error"; 16 | import { ImportFileNotFoundError } from "./errors/import-file-not-found-error"; 17 | import { BundleResultHasNoContentError } from "./errors/bundle-result-has-no-content-error"; 18 | import { OutFileNotDefinedError } from "./errors/out-file-not-defined-error"; 19 | import { EntryFileNotDefinedError } from "./errors/entry-file-not-defined-error"; 20 | import { renderScss } from "./utils/scss"; 21 | import { renderBundleInfo } from "./utils/bundle-info"; 22 | import { renderArchy } from "./utils/archy"; 23 | import { LogLevelDesc } from "loglevel"; 24 | import { resolveLogLevelKey, mergeObjects } from "./helpers"; 25 | 26 | const PACKAGE_JSON_PATH = path.resolve(__dirname, "../../package.json"); 27 | 28 | function bundleResultForEach(bundleResult: BundleResult, cb: (bundleResult: BundleResult) => void): void { 29 | cb(bundleResult); 30 | if (bundleResult.imports != null) { 31 | for (const bundleResultChild of bundleResult.imports) { 32 | bundleResultForEach(bundleResultChild, cb); 33 | } 34 | } 35 | } 36 | 37 | async function build( 38 | project: string | undefined, 39 | config: BundlerOptions 40 | ): Promise<{ bundleResult: BundleResult; fileRegistry: FileRegistry }> { 41 | if (config.entryFile == null) { 42 | throw new EntryFileNotDefinedError(); 43 | } 44 | 45 | if (config.outFile == null) { 46 | throw new OutFileNotDefinedError(); 47 | } 48 | 49 | const fileRegistry: FileRegistry = {}; 50 | const bundler = new Bundler(fileRegistry, project); 51 | const bundleResult = await bundler.bundle(config.entryFile, config.dedupeGlobs, config.includePaths, config.ignoreImports); 52 | 53 | if (!bundleResult.found) { 54 | throw new EntryFileNotFoundError(bundleResult.filePath); 55 | } 56 | 57 | bundleResultForEach(bundleResult, result => { 58 | if (!result.found && result.tilde && project == null) { 59 | Log.warn(`Found tilde import, but "project" was not specified.`); 60 | throw new ImportFileNotFoundError(result.filePath); 61 | } 62 | }); 63 | 64 | if (bundleResult.bundledContent == null) { 65 | throw new BundleResultHasNoContentError(); 66 | } 67 | 68 | await renderScss(project, config.includePaths, bundleResult.bundledContent); 69 | 70 | await fs.mkdirp(path.dirname(config.outFile)); 71 | await fs.writeFile(config.outFile, bundleResult.bundledContent); 72 | 73 | return { 74 | fileRegistry: fileRegistry, 75 | bundleResult: bundleResult 76 | }; 77 | } 78 | 79 | async function main(argv: string[]): Promise { 80 | const packageJson: { version: string } = await fs.readJson(PACKAGE_JSON_PATH); 81 | const cliOptions = resolveArguments(commander.version(packageJson.version, "-v, --version"), argv); 82 | 83 | let configLocation: string | undefined; 84 | if (cliOptions.config != null) { 85 | const stats = await fs.stat(cliOptions.config); 86 | if (stats.isDirectory()) { 87 | configLocation = path.resolve(cliOptions.config, CONFIG_FILE_NAME); 88 | } else { 89 | configLocation = cliOptions.config; 90 | } 91 | } 92 | // Resolve project location from CLI. 93 | let projectLocation: string | undefined; 94 | if (cliOptions.project != null) { 95 | const stats = await fs.stat(cliOptions.project); 96 | if (stats.isDirectory()) { 97 | projectLocation = cliOptions.project; 98 | } else { 99 | Log.warn(`[DEPRECATED]: Flag "project" pointing to the config file directly is deprecated. Provide a path to the directory where the project is.`); 100 | configLocation = cliOptions.project; 101 | projectLocation = path.dirname(cliOptions.project); 102 | } 103 | } 104 | 105 | let config: BundlerOptions; 106 | if (configLocation != null) { 107 | try { 108 | const jsonConfig = await resolveConfig(configLocation); 109 | 110 | config = mergeObjects(jsonConfig.bundlerOptions, cliOptions); 111 | } catch (error) { 112 | Log.error(error); 113 | process.exit(1); 114 | } 115 | } else { 116 | config = cliOptions; 117 | } 118 | 119 | // Resolve project location from config file. 120 | if (projectLocation == null && configLocation != null) { 121 | const configLocationDir = path.dirname(configLocation); 122 | projectLocation = path.resolve(configLocationDir, config.project ?? "./"); 123 | } 124 | 125 | let resolvedLogLevel: LogLevelDesc | undefined; 126 | if (config.logLevel != null) { 127 | const logLevelKey = resolveLogLevelKey(config.logLevel); 128 | if (logLevelKey != null) { 129 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 130 | resolvedLogLevel = LogLevel[logLevelKey as any] as LogLevelDesc; 131 | } 132 | } 133 | Log.setLevel(resolvedLogLevel == null ? LogLevel.Info : resolvedLogLevel); 134 | 135 | if (config.watch) { 136 | const onFileChange = debounce(async () => { 137 | Log.info("File changes detected."); 138 | 139 | await build(projectLocation, config); 140 | Log.info("Waiting for changes..."); 141 | }); 142 | 143 | if (!config.rootDir) { 144 | Log.warn(`rootDir property is missing in config, using current working directory: ${process.cwd()}`); 145 | } 146 | 147 | const watchFolder = config.rootDir ?? process.cwd(); 148 | 149 | Log.info("Waiting for changes..."); 150 | chokidar.watch(watchFolder).on("change", onFileChange); 151 | } else { 152 | try { 153 | const { fileRegistry, bundleResult } = await build(projectLocation, config); 154 | 155 | Log.info("Imports tree:"); 156 | Log.info(renderArchy(bundleResult, projectLocation)); 157 | 158 | Log.info(renderBundleInfo(bundleResult, fileRegistry)); 159 | } catch (error) { 160 | Log.error(error.message); 161 | process.exit(1); 162 | } 163 | } 164 | } 165 | 166 | main(process.argv); 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** 2 | > scss-bundle still works for use-cases without @use directive, but to support new SCSS module sytem [a bigger rewrite would be needed](https://github.com/reactway/scss-bundle/issues/90#issuecomment-619390804). Thus, we are not archiving the project to be read-only, but we will not contribute to it. In case someone wants to take it over, please open an issue. 3 | > If you use `scss-bundle` to export scss assets from an angular library, there is already [angular team solution](https://angular.io/guide/creating-libraries#managing-assets-in-a-library) 4 | 5 | # scss-bundle 6 | 7 | Bundles all SCSS imports into a single file recursively. 8 | 9 | [![NPM version](https://img.shields.io/npm/v/scss-bundle.svg?logo=npm)](https://www.npmjs.com/package/scss-bundle) 10 | [![NPM version](https://img.shields.io/npm/v/scss-bundle/canary.svg?logo=npm)](https://www.npmjs.com/package/scss-bundle/v/canary) 11 | 12 | [![Total downloads](https://img.shields.io/npm/dt/scss-bundle.svg)](https://www.npmjs.com/package/scss-bundle) 13 | [![Build Status](https://img.shields.io/azure-devops/build/reactway/reactway/13/master.svg?logo=azuredevops)](https://dev.azure.com/reactway/ReactWay/_build/latest?definitionId=13&branchName=master) 14 | [![Code coverage](https://img.shields.io/azure-devops/coverage/reactway/reactway/13/master.svg)](https://dev.azure.com/reactway/ReactWay/_build/latest?definitionId=13&branchName=master) 15 | 16 | [![Dependencies](https://img.shields.io/david/reactway/tiny-emitter.svg)](https://david-dm.org/reactway/scss-bundle) 17 | [![Dev dependencies](https://img.shields.io/david/dev/reactway/tiny-emitter.svg)](https://david-dm.org/reactway/scss-bundle?type=dev) 18 | 19 | ### Who uses `scss-bundle` 20 | 21 | #### Projects 22 | 23 | - [Angular/material2](https://github.com/angular/material2) 24 | - [Grassy](https://github.com/lazarljubenovic/grassy) 25 | 26 | #### Community plugins 27 | 28 | - [rollup-plugin-bundle-scss](https://github.com/weizhenye/rollup-plugin-bundle-scss) 29 | 30 | ## Get started 31 | 32 | If you want to use `scss-bundle` globally 33 | 34 | ```sh 35 | $ npm install scss-bundle -g 36 | ``` 37 | 38 | Latest dev build is published under `canary` tag. 39 | 40 | ```sh 41 | $ npm install scss-bundle@canary 42 | ``` 43 | 44 | To start using the tool, create a [config](#example-config) file and run command: 45 | 46 | ``` 47 | $ scss-bundle 48 | ``` 49 | 50 | It will bundle all scss files in specified `outFile` location. 51 | 52 | ## CLI Usage 53 | 54 | ```sh 55 | $ scss-bundle -h 56 | ``` 57 | 58 | ## Configuration 59 | 60 | Config file properties can be overridden with CLI flags. 61 | 62 | | CLI Flag | Bundler options | Type | Description | Values | Default | 63 | | --------------------------------------- | ------------------------ | -------- | ----------------------------------------------------------------- | ------------------------------------------ | ------- | 64 | | -c, --config \ | | string | Configuration file location. | | | 65 | | -p, --project \ | project | string | Project location where `node_modules` is located. | | | 66 | | -e, --entryFile \ `*` | entryFile `*` | string | Bundle entry file location. | | | 67 | | -o, --outFile \ `*` | outFile `*` | string | Bundle output location. | | | 68 | | --rootDir \ | rootDir | string | Specifies the root directory of input files. | | | 69 | | -w, --watch [boolean] | watch | boolean | Watch files for changes. Works with `rootDir`. | | | 70 | | --ignoreImports \ | ignoreImports | string[] | Ignore resolving import content by matching a regular expression. | | | 71 | | --includePaths \ | includePaths | string[] | Include paths for resolving imports. | | | 72 | | --dedupeGlobs \ | dedupeGlobs | string[] | Files that will be emitted in a bundle once. | | | 73 | | --logLevel \ | logLevel | string | Console log level. | silent, error, warning, info, debug, trace | info | 74 | 75 | `*` - Required 76 | 77 | ### Example config 78 | 79 | Default name for configuration is `scss-bundle.config.json`. 80 | 81 | ```json 82 | { 83 | "bundlerOptions": { 84 | "entryFile": "./tests/cases/simple/main.scss", 85 | "rootDir": "./tests/cases/simple/", 86 | "outFile": "./bundled.scss", 87 | "ignoreImports": ["~@angular/.*"], 88 | "logLevel": "silent" 89 | } 90 | } 91 | ``` 92 | 93 | ## Non-CLI usage 94 | 95 | ### Simple example 96 | 97 | ```typescript 98 | import path from "path"; 99 | import { Bundler } from "scss-bundle"; 100 | 101 | (async () => { 102 | // Absolute project directory path. 103 | const projectDirectory = path.resolve(__dirname, "./cases/tilde-import"); 104 | const bundler = new Bundler(undefined, projectDirectory); 105 | // Relative file path to project directory path. 106 | const result = await bundler.bundle("./main.scss"); 107 | })(); 108 | ``` 109 | 110 | # API 111 | 112 | ## Bundler 113 | 114 | ```typescript 115 | import { Bundler } from "scss-bundle"; 116 | ``` 117 | 118 | ### Constructor 119 | 120 | ```ts 121 | constructor(fileRegistry: FileRegistry = {}, projectDirectory?: string) {} 122 | ``` 123 | 124 | ##### Arguments 125 | 126 | - `fileRegistry?:` [Registry](#registry) - Dictionary of files contents by full path 127 | - `projectDirectory?: string` - Absolute project location, where `node_modules` are located. Used for resolving tilde imports 128 | 129 | ### Methods 130 | 131 | #### bundle 132 | 133 | ```typescript 134 | public async bundle(file: string, fileRegistry: Registry = {}): Promise 135 | ``` 136 | 137 | ##### Arguments 138 | 139 | - `file: string` - Main file full path 140 | - `fileRegistry:` [Registry](#registry) - Dictionary of files contents by full path 141 | 142 | ##### Returns 143 | 144 | `Promise<`[BundleResult](#bundleresult)`>` 145 | 146 | ### Contracts 147 | 148 | #### BundleResult 149 | 150 | ```typescript 151 | import { BundleResult } from "scss-bundle"; 152 | ``` 153 | 154 | ```typescript 155 | interface BundleResult { 156 | imports?: BundleResult[]; 157 | tilde?: boolean; 158 | deduped?: boolean; 159 | filePath: string; 160 | bundledContent?: string; 161 | found: boolean; 162 | ignored?: boolean; 163 | } 164 | ``` 165 | 166 | ##### Properties 167 | 168 | - `imports:` [BundleResult](#bundleresult)`[]` - File imports array 169 | - `tilde?: boolean` - Used tilde import 170 | - `filePath: string` - Full file path 171 | - `bundledContent?: string` - File content 172 | - `found: boolean` - Is file found 173 | 174 | #### Registry 175 | 176 | ```typescript 177 | import { Registry } from "scss-bundle"; 178 | ``` 179 | 180 | ```typescript 181 | interface Registry { 182 | [id: string]: string | undefined; 183 | } 184 | ``` 185 | 186 | ##### Key 187 | 188 | `id: string` - File full path as dictionary id 189 | 190 | ##### Value 191 | 192 | `string | undefined` - File content 193 | 194 | ## License 195 | 196 | Released under the [MIT license](LICENSE). 197 | -------------------------------------------------------------------------------- /src/bundler.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import os from "os"; 3 | import path from "path"; 4 | import globs from "globs"; 5 | 6 | import { matchAll } from "./helpers"; 7 | import { BundleResult, FileRegistry, ImportData } from "./contracts"; 8 | 9 | const IMPORT_PATTERN = /@import\s+['"](.+)['"];/g; 10 | const COMMENT_PATTERN = /\/\/.*$/gm; 11 | const MULTILINE_COMMENT_PATTERN = /\/\*[\s\S]*?\*\//g; 12 | const DEFAULT_FILE_EXTENSION = ".scss"; 13 | const ALLOWED_FILE_EXTENSIONS = [".scss", ".css"]; 14 | const NODE_MODULES = "node_modules"; 15 | const TILDE = "~"; 16 | 17 | export class Bundler { 18 | // Full paths of used imports and their count 19 | private usedImports: { [key: string]: number } = {}; 20 | // Imports dictionary by file 21 | private importsByFile: { [key: string]: BundleResult[] } = {}; 22 | 23 | constructor(private fileRegistry: FileRegistry = {}, private readonly projectDirectory?: string) {} 24 | 25 | public async bundle( 26 | file: string, 27 | dedupeGlobs: string[] = [], 28 | includePaths: string[] = [], 29 | ignoredImports: string[] = [] 30 | ): Promise { 31 | try { 32 | if (this.projectDirectory != null) { 33 | file = path.resolve(this.projectDirectory, file); 34 | } 35 | 36 | await fs.access(file); 37 | const contentPromise = fs.readFile(file, "utf-8"); 38 | const dedupeFilesPromise = this.globFilesOrEmpty(dedupeGlobs); 39 | 40 | // Await all async operations and extract results 41 | const [content, dedupeFiles] = await Promise.all([contentPromise, dedupeFilesPromise]); 42 | 43 | // Convert string array into regular expressions 44 | const ignoredImportsRegEx = ignoredImports.map(ignoredImport => new RegExp(ignoredImport)); 45 | 46 | return this._bundle(file, content, dedupeFiles, includePaths, ignoredImportsRegEx); 47 | } catch { 48 | return { 49 | filePath: file, 50 | found: false 51 | }; 52 | } 53 | } 54 | 55 | private isExtensionExists(importName: string): boolean { 56 | return ALLOWED_FILE_EXTENSIONS.some(extension => importName.indexOf(extension) !== -1); 57 | } 58 | private async _bundle( 59 | filePath: string, 60 | content: string, 61 | dedupeFiles: string[], 62 | includePaths: string[], 63 | ignoredImports: RegExp[] 64 | ): Promise { 65 | // Remove commented imports 66 | content = this.removeImportsFromComments(content); 67 | 68 | // Resolve path to work only with full paths 69 | filePath = path.resolve(filePath); 70 | 71 | const dirname = path.dirname(filePath); 72 | 73 | if (this.fileRegistry[filePath] == null) { 74 | this.fileRegistry[filePath] = content; 75 | } 76 | 77 | // Resolve imports file names (prepend underscore for partials) 78 | const importsPromises = matchAll(content, IMPORT_PATTERN).map(async match => { 79 | let importName = match[1]; 80 | // Append extension if it's absent 81 | if (!this.isExtensionExists(importName)) { 82 | importName += DEFAULT_FILE_EXTENSION; 83 | } 84 | 85 | // Determine if import should be ignored 86 | const ignored = ignoredImports.findIndex(ignoredImportRegex => ignoredImportRegex.test(importName)) !== -1; 87 | 88 | let fullPath: string; 89 | // Check for tilde import. 90 | const tilde: boolean = importName.startsWith(TILDE); 91 | if (tilde && this.projectDirectory != null) { 92 | importName = `./${NODE_MODULES}/${importName.substr(TILDE.length, importName.length)}`; 93 | fullPath = path.resolve(this.projectDirectory, importName); 94 | } else { 95 | fullPath = path.resolve(dirname, importName); 96 | } 97 | 98 | const importData: ImportData = { 99 | importString: match[0], 100 | tilde: tilde, 101 | path: importName, 102 | fullPath: fullPath, 103 | found: false, 104 | ignored: ignored 105 | }; 106 | 107 | await this.resolveImport(importData, includePaths); 108 | 109 | return importData; 110 | }); 111 | 112 | // Wait for all imports file names to be resolved 113 | const imports = await Promise.all(importsPromises); 114 | 115 | const bundleResult: BundleResult = { 116 | filePath: filePath, 117 | found: true 118 | }; 119 | 120 | const shouldCheckForDedupes = dedupeFiles != null && dedupeFiles.length > 0; 121 | 122 | // Bundle all imports 123 | const currentImports: BundleResult[] = []; 124 | for (const imp of imports) { 125 | let contentToReplace; 126 | 127 | let currentImport: BundleResult; 128 | 129 | // If neither import file, nor partial is found 130 | if (!imp.found) { 131 | // Add empty bundle result with found: false 132 | currentImport = { 133 | filePath: imp.fullPath, 134 | tilde: imp.tilde, 135 | found: false, 136 | ignored: imp.ignored 137 | }; 138 | } else if (this.usedImports[imp.fullPath] == null) { 139 | // Add it to used imports 140 | this.usedImports[imp.fullPath] = 1; 141 | 142 | // If file is not yet in the registry 143 | // Read 144 | const impContent = this.fileRegistry[imp.fullPath] == null 145 | ? await fs.readFile(imp.fullPath, "utf-8") 146 | : this.fileRegistry[imp.fullPath] as string; 147 | 148 | // and bundle it 149 | const bundledImport = await this._bundle(imp.fullPath, impContent, dedupeFiles, includePaths, ignoredImports); 150 | 151 | // Then add its bundled content to the registry 152 | this.fileRegistry[imp.fullPath] = bundledImport.bundledContent; 153 | 154 | // And whole BundleResult to current imports 155 | currentImport = bundledImport; 156 | } else { 157 | // File is in the registry 158 | // Increment it's usage count 159 | if (this.usedImports != null) { 160 | this.usedImports[imp.fullPath]++; 161 | } 162 | 163 | // Resolve child imports, if there are any 164 | let childImports: BundleResult[] = []; 165 | if (this.importsByFile != null) { 166 | childImports = this.importsByFile[imp.fullPath]; 167 | } 168 | 169 | // Construct and add result to current imports 170 | currentImport = { 171 | filePath: imp.fullPath, 172 | tilde: imp.tilde, 173 | found: true, 174 | imports: childImports 175 | }; 176 | } 177 | 178 | if (imp.ignored) { 179 | if (this.usedImports[imp.fullPath] > 1) { 180 | contentToReplace = ""; 181 | } else { 182 | contentToReplace = imp.importString; 183 | } 184 | } else { 185 | // Take contentToReplace from the fileRegistry 186 | contentToReplace = this.fileRegistry[imp.fullPath]; 187 | // If the content is not found 188 | if (contentToReplace == null) { 189 | // Indicate this with a comment for easier debugging 190 | contentToReplace = `/*** IMPORTED FILE NOT FOUND ***/${os.EOL}${imp.importString}/*** --- ***/`; 191 | } 192 | 193 | // If usedImports dictionary is defined 194 | if (shouldCheckForDedupes && this.usedImports != null) { 195 | // And current import path should be deduped and is used already 196 | const timesUsed = this.usedImports[imp.fullPath]; 197 | if (dedupeFiles.indexOf(imp.fullPath) !== -1 && timesUsed != null && timesUsed > 1) { 198 | // Reset content to replace to an empty string to skip it 199 | contentToReplace = ""; 200 | // And indicate that import was deduped 201 | currentImport.deduped = true; 202 | } 203 | } 204 | } 205 | // Finally, replace import string with bundled content or a debug message 206 | content = this.replaceLastOccurance(content, imp.importString, contentToReplace); 207 | 208 | // And push current import into the list 209 | currentImports.push(currentImport); 210 | } 211 | 212 | // Set result properties 213 | bundleResult.bundledContent = content; 214 | bundleResult.imports = currentImports; 215 | 216 | if (this.importsByFile != null) { 217 | this.importsByFile[filePath] = currentImports; 218 | } 219 | 220 | return bundleResult; 221 | } 222 | 223 | private replaceLastOccurance(content: string, importString: string, contentToReplace: string): string { 224 | const index = content.lastIndexOf(importString); 225 | return content.slice(0, index) + content.slice(index).replace(importString, contentToReplace); 226 | } 227 | 228 | private removeImportsFromComments(text: string): string { 229 | const patterns = [COMMENT_PATTERN, MULTILINE_COMMENT_PATTERN]; 230 | 231 | for (const pattern of patterns) { 232 | text = text.replace(pattern, x => x.replace(IMPORT_PATTERN, "")); 233 | } 234 | 235 | return text; 236 | } 237 | 238 | private async resolveImport(importData: ImportData, includePaths: string[]): Promise { 239 | if (this.fileRegistry[importData.fullPath]) { 240 | importData.found = true; 241 | return importData; 242 | } 243 | 244 | try { 245 | await fs.access(importData.fullPath); 246 | importData.found = true; 247 | } catch (error) { 248 | const underscoredDirname = path.dirname(importData.fullPath); 249 | const underscoredBasename = path.basename(importData.fullPath); 250 | const underscoredFilePath = path.join(underscoredDirname, `_${underscoredBasename}`); 251 | try { 252 | await fs.access(underscoredFilePath); 253 | importData.fullPath = underscoredFilePath; 254 | importData.found = true; 255 | } catch (underscoreErr) { 256 | // If there are any includePaths 257 | if (includePaths.length) { 258 | // Resolve fullPath using its first entry 259 | importData.fullPath = path.resolve(includePaths[0], importData.path); 260 | // Try resolving import with the remaining includePaths 261 | const remainingIncludePaths = includePaths.slice(1); 262 | return this.resolveImport(importData, remainingIncludePaths); 263 | } 264 | } 265 | } 266 | 267 | return importData; 268 | } 269 | 270 | private async globFilesOrEmpty(globsList: string[]): Promise { 271 | return new Promise((resolve, reject) => { 272 | if (globsList == null || globsList.length === 0) { 273 | resolve([]); 274 | return; 275 | } 276 | globs(globsList, (error: Error | null, files: string[]) => { 277 | if (error != null) { 278 | reject(error); 279 | } 280 | 281 | const fullPaths = files.map(file => path.resolve(file)); 282 | resolve(fullPaths); 283 | }); 284 | }); 285 | } 286 | } 287 | --------------------------------------------------------------------------------