From bc9536a2cb444b131497d3263131f7cf99486a12 Mon Sep 17 00:00:00 2001 From: Marco Solazzi Date: Mon, 9 Jan 2023 21:09:46 +0900 Subject: [PATCH] feat: improve sass compilation (#109) * feat(sass): allow importing from relative path and node_modules * fix test with todo * update usage * style: remove empty line * remove unused import Co-authored-by: Pooya Parsa --- package.json | 1 + pnpm-lock.yaml | 7 + src/loaders/sass.ts | 19 ++- test/fixture/src/_base.scss | 1 + test/fixture/src/components/js.vue | 7 +- test/fixture/src/demo.scss | 5 +- test/index.test.ts | 230 ++++++++++++++++------------- 7 files changed, 161 insertions(+), 109 deletions(-) create mode 100644 test/fixture/src/_base.scss diff --git a/package.json b/package.json index 9e7aa2c..af4ebf9 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/node": "^18.11.18", "@vitest/coverage-c8": "^0.26.3", "c8": "latest", + "modern-normalize": "^1.1.0", "eslint": "^8.31.0", "eslint-config-unjs": "^0.0.3", "prettier": "^2.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54012c3..90e0909 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: fs-extra: ^11.1.0 globby: ^13.1.3 jiti: ^1.16.1 + modern-normalize: ^1.1.0 mri: ^1.2.0 pathe: ^1.0.0 prettier: ^2.8.2 @@ -39,6 +40,7 @@ devDependencies: c8: 7.12.0 eslint: 8.31.0 eslint-config-unjs: 0.0.3_iukboom6ndih5an6iafl45j2fe + modern-normalize: 1.1.0 prettier: 2.8.2 sass: 1.57.1 standard-version: 9.5.0 @@ -3133,6 +3135,11 @@ packages: ufo: 1.0.0 dev: true + /modern-normalize/1.1.0: + resolution: {integrity: sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==} + engines: {node: '>=6'} + dev: true + /modify-values/1.0.1: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} diff --git a/src/loaders/sass.ts b/src/loaders/sass.ts index 682b494..7444049 100644 --- a/src/loaders/sass.ts +++ b/src/loaders/sass.ts @@ -1,3 +1,5 @@ +import { pathToFileURL } from "node:url"; +import { basename } from "pathe"; import type { Loader, LoaderResult } from "../loader"; export const sassLoader: Loader = async (input) => { @@ -5,6 +7,18 @@ export const sassLoader: Loader = async (input) => { return; } + // sass files starting with "_" are always considered partials + // and should not be compiled to standalone CSS + if (basename(input.srcPath).startsWith("_")) { + return [ + { + contents: "", + path: input.path, + skip: true, + }, + ]; + } + const compileString = await import("sass").then( (r) => r.compileString || r.default.compileString ); @@ -14,7 +28,10 @@ export const sassLoader: Loader = async (input) => { const contents = await input.getContents(); output.push({ - contents: compileString(contents).css, + contents: compileString(contents, { + loadPaths: ["node_modules"], + url: pathToFileURL(input.srcPath), + }).css, path: input.path, extension: ".css", }); diff --git a/test/fixture/src/_base.scss b/test/fixture/src/_base.scss new file mode 100644 index 0000000..1822b86 --- /dev/null +++ b/test/fixture/src/_base.scss @@ -0,0 +1 @@ +$color: green; diff --git a/test/fixture/src/components/js.vue b/test/fixture/src/components/js.vue index 6e0998f..4a16d88 100644 --- a/test/fixture/src/components/js.vue +++ b/test/fixture/src/components/js.vue @@ -16,9 +16,12 @@ export default { diff --git a/test/fixture/src/demo.scss b/test/fixture/src/demo.scss index d4f43ff..8bf401b 100644 --- a/test/fixture/src/demo.scss +++ b/test/fixture/src/demo.scss @@ -1,5 +1,6 @@ -$color: green; +@use "modern-normalize/modern-normalize.css"; +@use "base"; .test { - color: $color; + color: base.$color; } diff --git a/test/index.test.ts b/test/index.test.ts index c6a5ca2..d486c6f 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { resolve } from "pathe"; -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { mkdist } from "../src/make"; import { createLoader } from "../src/loader"; import { jsLoader, sassLoader, vueLoader } from "../src/loaders"; @@ -99,130 +99,152 @@ describe("mkdist", () => { ).toMatch("declare"); }, 50_000); - it("mkdist (sass compilation)", async () => { + describe("mkdist (sass compilation)", () => { const rootDir = resolve(__dirname, "fixture"); - await mkdist({ rootDir }); - const css = await readFile(resolve(rootDir, "dist/demo.css"), "utf8"); + let writtenFiles: string[]; + beforeEach(async () => { + const results = await mkdist({ rootDir }); + writtenFiles = results.writtenFiles; + }); - expect(css).toMatch("color: green"); - }); -}); + it("resolves local imports and excludes partials ", async () => { + const css = await readFile(resolve(rootDir, "dist/demo.css"), "utf8"); -describe("createLoader", () => { - it("loadFile returns undefined for an unsupported file", async () => { - const { loadFile } = createLoader(); - const results = await loadFile({ - extension: ".noth", - getContents: () => new Error("this should not be called") as any, - path: "another.noth", + expect(writtenFiles).not.toContain("dist/_base.css"); + expect(css).toMatch("color: green"); }); - expect(results).toMatchObject([{ raw: true }]); - }); - it("vueLoader handles no transpilation of script tag", async () => { - const { loadFile } = createLoader({ - loaders: [vueLoader], - }); - const results = await loadFile({ - extension: ".vue", - getContents: () => "", - path: "test.vue", + it("resolves node_modules imports", async () => { + const css = await readFile(resolve(rootDir, "dist/demo.css"), "utf8"); + expect(css).toMatch("box-sizing: border-box;"); }); - expect(results).toMatchObject([{ raw: true }]); - }); - it("vueLoader handles script tags with attributes", async () => { - const { loadFile } = createLoader({ - loaders: [vueLoader, jsLoader], - }); - const results = await loadFile({ - extension: ".vue", - getContents: () => '', - path: "test.vue", - }); - expect(results).toMatchObject([ - { contents: [""].join("\n") }, - ]); - }); + it("compiles sass blocks in vue SFC", async () => { + const vue = await readFile( + resolve(rootDir, "dist/components/js.vue"), + "utf8" + ); - it("vueLoader handles style tags", async () => { - const { loadFile } = createLoader({ - loaders: [vueLoader, sassLoader], - }); - const results = await loadFile({ - extension: ".vue", - getContents: () => - '', - path: "test.vue", + expect(vue).toMatch("color: green;\n background-color: red;"); }); - expect(results).toMatchObject([ - { - contents: [ - "", - ].join("\n"), - }, - ]); }); - it("vueLoader bypass ', - path: "test.vue", + + it("vueLoader handles no transpilation of script tag", async () => { + const { loadFile } = createLoader({ + loaders: [vueLoader], + }); + const results = await loadFile({ + extension: ".vue", + getContents: () => "", + path: "test.vue", + }); + expect(results).toMatchObject([{ raw: true }]); }); - expect(results).toMatchObject([{ raw: true }]); - }); - it("vueLoader will generate dts file", async () => { - const { loadFile } = createLoader({ - loaders: [vueLoader, jsLoader], - declaration: true, + it("vueLoader handles script tags with attributes", async () => { + const { loadFile } = createLoader({ + loaders: [vueLoader, jsLoader], + }); + const results = await loadFile({ + extension: ".vue", + getContents: () => '', + path: "test.vue", + }); + expect(results).toMatchObject([ + { contents: [""].join("\n") }, + ]); }); - const results = await loadFile({ - extension: ".vue", - getContents: () => - '', - path: "test.vue", + + it("vueLoader handles style tags", async () => { + const { loadFile } = createLoader({ + loaders: [vueLoader, sassLoader], + }); + const results = await loadFile({ + extension: ".vue", + getContents: () => + '', + path: "test.vue", + }); + expect(results).toMatchObject([ + { + contents: [ + "", + ].join("\n"), + }, + ]); }); - expect(results).toEqual( - expect.arrayContaining([expect.objectContaining({ declaration: true })]) - ); - }); - it("jsLoader will generate dts file (.js)", async () => { - const { loadFile } = createLoader({ - loaders: [jsLoader], - declaration: true, + it("vueLoader bypass ', + path: "test.vue", + }); + expect(results).toMatchObject([{ raw: true }]); }); - const results = await loadFile({ - extension: ".js", - getContents: () => "export default bob = 42", - path: "test.mjs", + + it("vueLoader will generate dts file", async () => { + const { loadFile } = createLoader({ + loaders: [vueLoader, jsLoader], + declaration: true, + }); + const results = await loadFile({ + extension: ".vue", + getContents: () => + '', + path: "test.vue", + }); + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining({ declaration: true })]) + ); }); - expect(results).toEqual( - expect.arrayContaining([expect.objectContaining({ declaration: true })]) - ); - }); - it("jsLoader will generate dts file (.ts)", async () => { - const { loadFile } = createLoader({ - loaders: [jsLoader], - declaration: true, + it("jsLoader will generate dts file (.js)", async () => { + const { loadFile } = createLoader({ + loaders: [jsLoader], + declaration: true, + }); + const results = await loadFile({ + extension: ".js", + getContents: () => "export default bob = 42", + path: "test.mjs", + }); + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining({ declaration: true })]) + ); }); - const results = await loadFile({ - extension: ".ts", - getContents: () => "export default bob = 42 as const", - path: "test.ts", + + it("jsLoader will generate dts file (.ts)", async () => { + const { loadFile } = createLoader({ + loaders: [jsLoader], + declaration: true, + }); + const results = await loadFile({ + extension: ".ts", + getContents: () => "export default bob = 42 as const", + path: "test.ts", + }); + expect(results).toEqual( + expect.arrayContaining([expect.objectContaining({ declaration: true })]) + ); }); - expect(results).toEqual( - expect.arrayContaining([expect.objectContaining({ declaration: true })]) - ); }); });