Skip to content

Commit

Permalink
feat: improve sass compilation (#109)
Browse files Browse the repository at this point in the history
* 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 <pooya@pi0.io>
  • Loading branch information
dwightjack and pi0 authored Jan 9, 2023
1 parent 338f65f commit bc9536a
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 109 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion src/loaders/sass.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { pathToFileURL } from "node:url";
import { basename } from "pathe";
import type { Loader, LoaderResult } from "../loader";

export const sassLoader: Loader = async (input) => {
if (![".sass", ".scss"].includes(input.extension)) {
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
);
Expand All @@ -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",
});
Expand Down
1 change: 1 addition & 0 deletions test/fixture/src/_base.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$color: green;
7 changes: 5 additions & 2 deletions test/fixture/src/components/js.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ export default {
</script>

<style lang="scss" scoped>
$color: red;
@use "../base" as base;
$bg-color: red;
.test {
color: $color;
color: base.$color;
background-color: $bg-color;
}
</style>
5 changes: 3 additions & 2 deletions test/fixture/src/demo.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
$color: green;
@use "modern-normalize/modern-normalize.css";
@use "base";

.test {
color: $color;
color: base.$color;
}
230 changes: 126 additions & 104 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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: () => "<script>Test</script>",
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: () => '<script foo lang="ts">Test</script>',
path: "test.vue",
});
expect(results).toMatchObject([
{ contents: ["<script foo>", "Test;", "</script>"].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: () =>
'<style scoped lang="scss">$color: red; :root { background-color: $color }</style>',
path: "test.vue",
expect(vue).toMatch("color: green;\n background-color: red;");
});
expect(results).toMatchObject([
{
contents: [
"<style scoped>",
":root {",
" background-color: red;",
"}",
"</style>",
].join("\n"),
},
]);
});

it("vueLoader bypass <script setup>", async () => {
const { loadFile } = createLoader({
loaders: [vueLoader, jsLoader],
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(results).toMatchObject([{ raw: true }]);
});
const results = await loadFile({
extension: ".vue",
getContents: () => '<script lang="ts" setup>Test</script>',
path: "test.vue",

it("vueLoader handles no transpilation of script tag", async () => {
const { loadFile } = createLoader({
loaders: [vueLoader],
});
const results = await loadFile({
extension: ".vue",
getContents: () => "<script>Test</script>",
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: () => '<script foo lang="ts">Test</script>',
path: "test.vue",
});
expect(results).toMatchObject([
{ contents: ["<script foo>", "Test;", "</script>"].join("\n") },
]);
});
const results = await loadFile({
extension: ".vue",
getContents: () =>
'<script lang="ts">export default bob = 42 as const</script>',
path: "test.vue",

it("vueLoader handles style tags", async () => {
const { loadFile } = createLoader({
loaders: [vueLoader, sassLoader],
});
const results = await loadFile({
extension: ".vue",
getContents: () =>
'<style scoped lang="scss">$color: red; :root { background-color: $color }</style>',
path: "test.vue",
});
expect(results).toMatchObject([
{
contents: [
"<style scoped>",
":root {",
" background-color: red;",
"}",
"</style>",
].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 <script setup>", async () => {
const { loadFile } = createLoader({
loaders: [vueLoader, jsLoader],
});
const results = await loadFile({
extension: ".vue",
getContents: () => '<script lang="ts" setup>Test</script>',
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: () =>
'<script lang="ts">export default bob = 42 as const</script>',
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 })])
);
});
});

0 comments on commit bc9536a

Please sign in to comment.