Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve sass compilation #109

Merged
merged 7 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 })])
);
});
});