@moritzloewenstein/vite-plugin-sass-glob-import 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ name: Lint and Test
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+
16
+ - uses: actions/setup-node@v5
17
+ with:
18
+ node-version: 22.x
19
+ cache: 'npm'
20
+
21
+ - uses: actions/cache@v4
22
+ with:
23
+ path: ~/.npm
24
+ key: npm-${{ hashFiles('package-lock.json') }}
25
+ restore-keys: npm-
26
+
27
+ - name: Install dependencies
28
+ run: npm ci
29
+
30
+ - name: Run Biome
31
+ run: npm run biome:ci
32
+
33
+ - name: Run tests
34
+ run: npm run test
@@ -0,0 +1,38 @@
1
+ name: Publish
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["Lint and Test"]
6
+ branches: [main]
7
+ types:
8
+ - completed
9
+
10
+ permissions:
11
+ id-token: write
12
+ contents: read
13
+
14
+ jobs:
15
+ publish:
16
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - uses: actions/checkout@v5
21
+ - uses: actions/setup-node@v5
22
+ with:
23
+ node-version: '22.x'
24
+ registry-url: 'https://registry.npmjs.org'
25
+ - uses: actions/cache@v4
26
+ with:
27
+ path: ~/.npm
28
+ key: npm-${{ hashFiles('package-lock.json') }}
29
+ restore-keys: npm-
30
+ - name: Install deps and build
31
+ run: |
32
+ set -e
33
+ npm ci
34
+ npm run build
35
+ - name: Publish to npm
36
+ run: npm publish --tag latest --access public --provenance
37
+ env:
38
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 20
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-present, Malven LLC
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.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ ## Note
2
+
3
+ Fork of [vite-plugin-sass-glob-import](https://github.com/cmalven/vite-plugin-sass-glob-import) which implements some stuff:
4
+
5
+ - Use namespaces (`@use "foo/a/bar.scss" as bar_1;`) to glob import multiple files with the same name (`@use "./foo/*/bar.scss";`)
6
+ - TODO: vite-rolldown hook filters: https://rolldown.rs/plugins/hook-filters
7
+
8
+ # vite-plugin-sass-glob-import
9
+
10
+ > Use glob syntax for @import or @use in your main Sass or SCSS file.
11
+
12
+ ## Install
13
+
14
+ ```shell
15
+ npm i -D vite-plugin-sass-glob-import
16
+ ```
17
+
18
+ ```js
19
+ // In vite.config.js
20
+
21
+ import { defineConfig } from "vite";
22
+ import sassGlobImports from "vite-plugin-sass-glob-import";
23
+
24
+ export default defineConfig({
25
+ plugins: [sassGlobImports()],
26
+ });
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ **Note:** Globbing only work in a top-level file, not within referenced files.
32
+
33
+ ```scss
34
+ // In src/styles/main.scss
35
+
36
+ @use "vars/**/*.scss";
37
+ @import "utils/**/*.scss";
38
+ @import "objects/**/*.scss";
39
+ ```
40
+
41
+ The above will be transformed into something like the following before Vite processes it with Sass:
42
+
43
+ ```scss
44
+ @use "vars/var-a.scss";
45
+ @use "vars/var-b.scss";
46
+ @import "utils/utils-a.scss";
47
+ @import "utils/utils-b.scss";
48
+ @import "objects/objects-a.scss";
49
+ @import "objects/objects-b.scss";
50
+ @import "objects/objects-c.scss";
51
+ ```
52
+
53
+ ## Caveats
54
+
55
+ This plugin is intentionally simple and doesn't attempt to support every feature offered by Vite. If your use-case isn't similar to the examples in the README above, it probably isn't supported by this plugin.
package/biome.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "node_modules/@biomejs/biome/configuration_schema.json",
3
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
4
+ "files": {
5
+ "ignoreUnknown": true,
6
+ "includes": ["**", "!dist"]
7
+ },
8
+ "vcs": {
9
+ "enabled": true,
10
+ "clientKind": "git",
11
+ "defaultBranch": "main",
12
+ "useIgnoreFile": true
13
+ },
14
+ "linter": {
15
+ "enabled": true,
16
+ "rules": {
17
+ "recommended": true,
18
+ "complexity": {
19
+ "noForEach": "off"
20
+ },
21
+ "style": {
22
+ "noParameterAssign": "error",
23
+ "useAsConstAssertion": "error",
24
+ "useDefaultParameterLast": "error",
25
+ "useEnumInitializers": "error",
26
+ "useSelfClosingElements": "error",
27
+ "useSingleVarDeclarator": "error",
28
+ "noUnusedTemplateLiteral": "error",
29
+ "useNumberNamespace": "error",
30
+ "noInferrableTypes": "error",
31
+ "noUselessElse": "error"
32
+ }
33
+ }
34
+ },
35
+ "formatter": {
36
+ "enabled": true
37
+ },
38
+ "css": {
39
+ "formatter": {
40
+ "enabled": false
41
+ },
42
+ "linter": {
43
+ "enabled": false
44
+ }
45
+ },
46
+ "graphql": {
47
+ "formatter": {
48
+ "enabled": false
49
+ },
50
+ "linter": {
51
+ "enabled": false
52
+ }
53
+ }
54
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@moritzloewenstein/vite-plugin-sass-glob-import",
3
+ "version": "5.0.0",
4
+ "description": "Use glob syntax for imports in your main Sass or SCSS file.",
5
+ "main": "./dist/index.mjs",
6
+ "types": "./dist/index.d.mts",
7
+ "scripts": {
8
+ "dev": "vitest",
9
+ "build": "tsup src/index.ts --format esm --dts",
10
+ "test": "vitest run",
11
+ "biome:check": "biome check .",
12
+ "biome:write": "biome check . --write",
13
+ "biome:ci": "biome ci"
14
+ },
15
+ "author": {
16
+ "name": "Malven Co.",
17
+ "email": "chris@malven.co",
18
+ "url": "https://malven.co"
19
+ },
20
+ "keywords": [
21
+ "vite",
22
+ "vite-plugin",
23
+ "sass"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/MoritzLoewenstein/vite-plugin-sass-glob-import.git"
28
+ },
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "ansi-colors": "^4.1.3",
32
+ "glob": "^11.0.3",
33
+ "minimatch": "^10.0.3"
34
+ },
35
+ "peerDependencies": {
36
+ "vite": "^5.0.0 || ^6.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@biomejs/biome": "^2.2.4",
40
+ "@types/glob": "^8.1.0",
41
+ "@types/node": "^22.18.6",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.9.2",
44
+ "vite": "^6.3.6",
45
+ "vitest": "^2.1.9"
46
+ },
47
+ "volta": {
48
+ "node": "22.19.0"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import c from "ansi-colors";
4
+ import { globSync } from "glob";
5
+ import { minimatch } from "minimatch";
6
+ import type { Plugin } from "vite";
7
+ import type { PluginOptions, TransformResult } from "./types";
8
+
9
+ let IMPORT_REGEX: RegExp;
10
+ let options: PluginOptions;
11
+ export default function sassGlobImports(_options: PluginOptions = {}): Plugin {
12
+ IMPORT_REGEX =
13
+ /^([ \t]*(?:\/\*.*)?)@(import|use)\s+["']([^"']+\*[^"']*(?:\.scss|\.sass)?)["'];?([ \t]*(?:\/[/*].*)?)$/gm;
14
+ options = _options;
15
+ return {
16
+ name: "sass-glob-import",
17
+ enforce: "pre",
18
+ transform(src: string, id: string): TransformResult {
19
+ const fileName = path.basename(id);
20
+ const filePath = path.dirname(id);
21
+ return {
22
+ code: transform(src, fileName, filePath),
23
+ map: null, // provide source map if available
24
+ };
25
+ },
26
+ };
27
+ }
28
+
29
+ function isSassOrScss(filename: string) {
30
+ return (
31
+ !fs.statSync(filename).isDirectory() &&
32
+ path.extname(filename).match(/\.sass|\.scss/i)
33
+ );
34
+ }
35
+
36
+ function transform(src: string, fileName: string, filePath: string): string {
37
+ // Determine if this is Sass (vs SCSS) based on file extension
38
+ const isSass = path.extname(fileName).match(/\.sass/i);
39
+
40
+ // Store base locations
41
+ const searchBases = [filePath];
42
+ const ignorePaths = options.ignorePaths || [];
43
+ const contentLinesCount = src.split("\n").length;
44
+
45
+ for (let i = 0; i < contentLinesCount; i++) {
46
+ const result = [...src.matchAll(IMPORT_REGEX)];
47
+ if (result.length === 0) continue;
48
+
49
+ const [importRule, startComment, importType, globPattern, endComment] =
50
+ result[0];
51
+
52
+ let files: string[] = [];
53
+ let basePath = "";
54
+ for (let i = 0; i < searchBases.length; i++) {
55
+ basePath = searchBases[i];
56
+
57
+ files = globSync(path.join(basePath, globPattern), {
58
+ cwd: "./",
59
+ windowsPathsNoEscape: true,
60
+ }).sort((a, b) => a.localeCompare(b, "en"));
61
+
62
+ // Do directories exist matching the glob pattern?
63
+ const globPatternWithoutWildcard = globPattern.split("*")[0];
64
+ if (globPatternWithoutWildcard.length) {
65
+ const directoryExists = fs.existsSync(
66
+ path.join(basePath, globPatternWithoutWildcard),
67
+ );
68
+ if (!directoryExists) {
69
+ console.warn(
70
+ c.yellow(
71
+ `Sass Glob Import: Directories don't exist for the glob pattern "${globPattern}"`,
72
+ ),
73
+ );
74
+ }
75
+ }
76
+
77
+ if (files.length > 0) {
78
+ break;
79
+ }
80
+ }
81
+
82
+ const isGlobTrailStatic = !globPattern.split("/").at(-1)?.includes("*");
83
+ const imports = [];
84
+ files.forEach((anyFilename: string, index: number) => {
85
+ if (!isSassOrScss(anyFilename)) {
86
+ return;
87
+ }
88
+
89
+ const scssFilename = path
90
+ // Remove parent base path
91
+ .relative(basePath, anyFilename)
92
+ .replace(/\\/g, "/")
93
+ // Remove leading slash
94
+ .replace(/^\//, "");
95
+ if (
96
+ !ignorePaths.some((ignorePath: string) => {
97
+ return minimatch(scssFilename, ignorePath);
98
+ })
99
+ ) {
100
+ const file = isGlobTrailStatic
101
+ ? `"${scssFilename}" as ${path.parse(scssFilename).name}_${index}`
102
+ : `"${scssFilename}"`;
103
+ imports.push(`@${importType} ${file}${isSass ? "" : ";"}`);
104
+ }
105
+ });
106
+
107
+ if (startComment) {
108
+ imports.unshift(startComment);
109
+ }
110
+
111
+ if (endComment) {
112
+ imports.push(endComment);
113
+ }
114
+
115
+ const replaceString = imports.join("\n");
116
+ // biome-ignore lint: easier for now
117
+ src = src.replace(importRule, replaceString);
118
+ }
119
+
120
+ // Return the transformed source
121
+ return src;
122
+ }
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface PluginOptions {
2
+ ignorePaths?: string[];
3
+ }
4
+
5
+ export interface TransformResult {
6
+ code: string;
7
+ map: null;
8
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import sassGlobImportPlugin from "../src";
3
+
4
+ const source = `
5
+ body {}
6
+ @import "files/*.scss";
7
+ `;
8
+
9
+ describe("it correctly converts glob patterns to inline imports", () => {
10
+ // biome-ignore lint: TODO
11
+ const plugin: any = sassGlobImportPlugin();
12
+
13
+ it("for SCSS", () => {
14
+ const expected = `
15
+ body {}
16
+ @import "files/_file-a.scss";
17
+ @import "files/_file-b.scss";
18
+ `;
19
+ const path = `${__dirname}/virtual-file.scss`;
20
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
21
+ });
22
+
23
+ it("for Sass", () => {
24
+ const expected = `
25
+ body {}
26
+ @import "files/_file-a.scss"
27
+ @import "files/_file-b.scss"
28
+ `;
29
+ const path = `${__dirname}/virtual-file.sass`;
30
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
31
+ });
32
+
33
+ it("with @use", () => {
34
+ const source = `
35
+ body {}
36
+ @use "files/*.scss";
37
+ `;
38
+ const expected = `
39
+ body {}
40
+ @use "files/_file-a.scss";
41
+ @use "files/_file-b.scss";
42
+ `;
43
+ const path = `${__dirname}/virtual-file.scss`;
44
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
45
+ });
46
+ });
47
+
48
+ describe("it warns for invalid glob paths", () => {
49
+ // biome-ignore lint: TODO
50
+ const plugin: any = sassGlobImportPlugin();
51
+
52
+ it("for SCSS", () => {
53
+ const source = `
54
+ body {}
55
+ @use "foo/**/*.scss";
56
+ `;
57
+ const expected = `
58
+ body {}
59
+
60
+ `;
61
+ const path = `${__dirname}/virtual-file.scss`;
62
+ vi.spyOn(console, "warn");
63
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
64
+ expect(console.warn).toHaveBeenCalledTimes(1);
65
+ });
66
+ });
67
+
68
+ describe("it correctly converts glob patterns with static trail to namespace imports", () => {
69
+ // biome-ignore lint: TODO
70
+ const plugin: any = sassGlobImportPlugin();
71
+
72
+ it.todo("for SCSS", () => {
73
+ //TODO does this even work?
74
+ const expected = `
75
+ body {}
76
+ @import "files/a/foo.scss";
77
+ @import "files/b/foo.scss";
78
+ `;
79
+ const path = `${__dirname}/virtual-file.scss`;
80
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
81
+ });
82
+
83
+ it.todo("for Sass", () => {
84
+ //TODO does this even work?
85
+ const expected = `
86
+ body {}
87
+ @import "files/a/foo.scss"
88
+ @import "files/b/foo.scss"
89
+ `;
90
+ const path = `${__dirname}/virtual-file.sass`;
91
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
92
+ });
93
+
94
+ it("with @use", () => {
95
+ const source = `
96
+ body {}
97
+ @use "files/*/foo.scss";
98
+ `;
99
+ const expected = `
100
+ body {}
101
+ @use "files/a/foo.scss" as foo_0;
102
+ @use "files/b/foo.scss" as foo_1;
103
+ `;
104
+ const path = `${__dirname}/virtual-file.scss`;
105
+ expect(plugin.transform(source, path)?.code).toEqual(expected);
106
+ });
107
+ });
File without changes
File without changes
File without changes
File without changes
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "lib": ["ESNext", "DOM"],
6
+ "moduleResolution": "Node",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true,
9
+ "sourceMap": true
10
+ },
11
+ "include": ["./src"]
12
+ }