diff --git a/node/test/bundle.test.mjs b/node/test/bundle.test.mjs
deleted file mode 100644
index 50d113b5..00000000
--- a/node/test/bundle.test.mjs
+++ /dev/null
@@ -1,417 +0,0 @@
-import path from 'path';
-import fs from 'fs';
-import { test } from 'uvu';
-import * as assert from 'uvu/assert';
-import {webcrypto as crypto} from 'node:crypto';
-
-let bundleAsync;
-if (process.env.TEST_WASM === 'node') {
- bundleAsync = (await import('../../wasm/wasm-node.mjs')).bundleAsync;
-} else if (process.env.TEST_WASM === 'browser') {
- // Define crypto globally for old node.
- // @ts-ignore
- globalThis.crypto ??= crypto;
- let wasm = await import('../../wasm/index.mjs');
- await wasm.default();
- bundleAsync = function (options) {
- if (!options.resolver?.read) {
- options.resolver = {
- ...options.resolver,
- read: (filePath) => fs.readFileSync(filePath, 'utf8')
- };
- }
-
- return wasm.bundleAsync(options);
- }
-} else {
- bundleAsync = (await import('../index.mjs')).bundleAsync;
-}
-
-test('resolver', async () => {
- const inMemoryFs = new Map(Object.entries({
- 'foo.css': `
- @import 'root:bar.css';
-
- .foo { color: red; }
- `.trim(),
-
- 'bar.css': `
- @import 'root:hello/world.css';
-
- .bar { color: green; }
- `.trim(),
-
- 'hello/world.css': `
- .baz { color: blue; }
- `.trim(),
- }));
-
- const { code: buffer } = await bundleAsync({
- filename: 'foo.css',
- resolver: {
- read(file) {
- const result = inMemoryFs.get(path.normalize(file));
- if (!result) throw new Error(`Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(', ')}.`);
- return result;
- },
-
- resolve(specifier) {
- return specifier.slice('root:'.length);
- },
- },
- });
- const code = buffer.toString('utf-8').trim();
-
- const expected = `
-.baz {
- color: #00f;
-}
-
-.bar {
- color: green;
-}
-
-.foo {
- color: red;
-}
- `.trim();
- if (code !== expected) throw new Error(`\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
-});
-
-test('only custom read', async () => {
- const inMemoryFs = new Map(Object.entries({
- 'foo.css': `
- @import 'hello/world.css';
-
- .foo { color: red; }
- `.trim(),
-
- 'hello/world.css': `
- @import '../bar.css';
-
- .bar { color: green; }
- `.trim(),
-
- 'bar.css': `
- .baz { color: blue; }
- `.trim(),
- }));
-
- const { code: buffer } = await bundleAsync({
- filename: 'foo.css',
- resolver: {
- read(file) {
- const result = inMemoryFs.get(path.normalize(file));
- if (!result) throw new Error(`Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(', ')}.`);
- return result;
- },
- },
- });
- const code = buffer.toString('utf-8').trim();
-
- const expected = `
-.baz {
- color: #00f;
-}
-
-.bar {
- color: green;
-}
-
-.foo {
- color: red;
-}
- `.trim();
- if (code !== expected) throw new Error(`\`testOnlyCustomRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
-});
-
-test('only custom resolve', async () => {
- const root = path.join('tests', 'testdata');
- const { code: buffer } = await bundleAsync({
- filename: path.join(root, 'foo.css'),
- resolver: {
- resolve(specifier) {
- // Strip `root:` prefix off specifier and resolve it as an absolute path
- // in the test data root.
- return path.join(root, specifier.slice('root:'.length));
- },
- },
- });
- const code = buffer.toString('utf-8').trim();
-
- const expected = `
-.baz {
- color: #00f;
-}
-
-.bar {
- color: green;
-}
-
-.foo {
- color: red;
-}
- `.trim();
- if (code !== expected) throw new Error(`\`testOnlyCustomResolve()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
-});
-
-test('async read', async () => {
- const root = path.join('tests', 'testdata');
- const { code: buffer } = await bundleAsync({
- filename: path.join(root, 'foo.css'),
- resolver: {
- async read(file) {
- return await fs.promises.readFile(file, 'utf8');
- },
- resolve(specifier) {
- // Strip `root:` prefix off specifier and resolve it as an absolute path
- // in the test data root.
- return path.join(root, specifier.slice('root:'.length));
- },
- },
- });
- const code = buffer.toString('utf-8').trim();
-
- const expected = `
-.baz {
- color: #00f;
-}
-
-.bar {
- color: green;
-}
-
-.foo {
- color: red;
-}
- `.trim();
- if (code !== expected) throw new Error(`\`testAsyncRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`);
-});
-
-test('read throw', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'foo.css',
- resolver: {
- read(file) {
- throw new Error(`Oh noes! Failed to read \`${file}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to read \`foo.css\`.`);
- assert.equal(error.loc, undefined); // error occurred when reading initial file, no location info available.
-});
-
-test('async read throw', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'foo.css',
- resolver: {
- async read(file) {
- throw new Error(`Oh noes! Failed to read \`${file}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to read \`foo.css\`.`);
- assert.equal(error.loc, undefined); // error occurred when reading initial file, no location info available.
-});
-
-test('read throw with location info', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'foo.css',
- resolver: {
- read(file) {
- if (file === 'foo.css') {
- return '@import "bar.css"';
- }
- throw new Error(`Oh noes! Failed to read \`${file}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to read \`bar.css\`.`);
- assert.equal(error.fileName, 'foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 1
- });
-});
-
-test('async read throw with location info', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'foo.css',
- resolver: {
- async read(file) {
- if (file === 'foo.css') {
- return '@import "bar.css"';
- }
- throw new Error(`Oh noes! Failed to read \`${file}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to read \`bar.css\`.`);
- assert.equal(error.fileName, 'foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 1
- });
-});
-
-test('resolve throw', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'tests/testdata/foo.css',
- resolver: {
- resolve(specifier, originatingFile) {
- throw new Error(`Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`);
- assert.equal(error.fileName, 'tests/testdata/foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 1
- });
-});
-
-test('async resolve throw', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'tests/testdata/foo.css',
- resolver: {
- async resolve(specifier, originatingFile) {
- throw new Error(`Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`);
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`);
- assert.equal(error.fileName, 'tests/testdata/foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 1
- });
-});
-
-test('read return non-string', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'foo.css',
- resolver: {
- read() {
- return 1234; // Returns a non-string value.
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testReadReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, 'expect String, got: Number');
-});
-
-test('resolve return non-string', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'tests/testdata/foo.css',
- resolver: {
- resolve() {
- return 1234; // Returns a non-string value.
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- if (!error) throw new Error(`\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`);
- assert.equal(error.message, 'expect String, got: Number');
- assert.equal(error.fileName, 'tests/testdata/foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 1
- });
-});
-
-test('should throw with location info on syntax errors', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'tests/testdata/foo.css',
- resolver: {
- read() {
- return '.foo'
- }
- },
- });
- } catch (err) {
- error = err;
- }
-
- assert.equal(error.message, `Unexpected end of input`);
- assert.equal(error.fileName, 'tests/testdata/foo.css');
- assert.equal(error.loc, {
- line: 1,
- column: 5
- });
-});
-
-test('should support throwing in visitors', async () => {
- let error = undefined;
- try {
- await bundleAsync({
- filename: 'tests/testdata/a.css',
- visitor: {
- Rule() {
- throw new Error('Some error')
- }
- }
- });
- } catch (err) {
- error = err;
- }
-
- assert.equal(error.message, 'Some error');
-});
-
-test.run();
diff --git a/node/test/bundle.test.ts b/node/test/bundle.test.ts
new file mode 100644
index 00000000..4f5b5fea
--- /dev/null
+++ b/node/test/bundle.test.ts
@@ -0,0 +1,480 @@
+///
+
+import path from 'node:path';
+import fs from 'node:fs';
+import {beforeAll, expect, test} from 'vitest';
+import type {bundleAsync as BundleAsync, Warning} from '../index.js';
+
+let bundleAsync: typeof BundleAsync;
+
+// TODO: export from library?
+interface BundleAsyncError extends Warning {
+ fileName: string;
+}
+
+beforeAll(async () => {
+ if (import.meta.env.TEST_WASM === 'node') {
+ bundleAsync = (await import('../../wasm/wasm-node.mjs')).bundleAsync;
+ } else if (import.meta.env.TEST_WASM === 'browser') {
+ let wasm = await import('../../wasm/index.mjs');
+ await wasm.default();
+ bundleAsync = function (options) {
+ if (!options.resolver?.read) {
+ options.resolver = {
+ ...options.resolver,
+ read: (filePath) => fs.readFileSync(filePath, 'utf8'),
+ };
+ }
+
+ return wasm.bundleAsync(options);
+ };
+ } else {
+ bundleAsync = (await import('../index.mjs')).bundleAsync;
+ }
+});
+
+test('resolver', async () => {
+ const inMemoryFs = new Map(
+ Object.entries({
+ 'foo.css': `
+ @import 'root:bar.css';
+
+ .foo { color: red; }
+ `.trim(),
+
+ 'bar.css': `
+ @import 'root:hello/world.css';
+
+ .bar { color: green; }
+ `.trim(),
+
+ 'hello/world.css': `
+ .baz { color: blue; }
+ `.trim(),
+ }),
+ );
+
+ const {code: buffer} = await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ read(file) {
+ const result = inMemoryFs.get(path.normalize(file));
+ if (!result)
+ throw new Error(
+ `Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(
+ ', ',
+ )}.`,
+ );
+ return result;
+ },
+
+ resolve(specifier) {
+ return specifier.slice('root:'.length);
+ },
+ },
+ });
+ const code = buffer.toString().trim();
+
+ const expected = `
+.baz {
+ color: #00f;
+}
+
+.bar {
+ color: green;
+}
+
+.foo {
+ color: red;
+}
+ `.trim();
+ if (code !== expected)
+ throw new Error(
+ `\`testResolver()\` failed. Expected:\n${expected}\n\nGot:\n${code}`,
+ );
+});
+
+test('only custom read', async () => {
+ const inMemoryFs = new Map(
+ Object.entries({
+ 'foo.css': `
+ @import 'hello/world.css';
+
+ .foo { color: red; }
+ `.trim(),
+
+ 'hello/world.css': `
+ @import '../bar.css';
+
+ .bar { color: green; }
+ `.trim(),
+
+ 'bar.css': `
+ .baz { color: blue; }
+ `.trim(),
+ }),
+ );
+
+ const {code: buffer} = await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ read(file) {
+ const result = inMemoryFs.get(path.normalize(file));
+ if (!result)
+ throw new Error(
+ `Could not find ${file} in ${Array.from(inMemoryFs.keys()).join(
+ ', ',
+ )}.`,
+ );
+ return result;
+ },
+ },
+ });
+ const code = buffer.toString().trim();
+
+ const expected = `
+.baz {
+ color: #00f;
+}
+
+.bar {
+ color: green;
+}
+
+.foo {
+ color: red;
+}
+ `.trim();
+ if (code !== expected)
+ throw new Error(
+ `\`testOnlyCustomRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`,
+ );
+});
+
+test('only custom resolve', async () => {
+ const root = path.join('tests', 'testdata');
+ const {code: buffer} = await bundleAsync({
+ filename: path.join(root, 'foo.css'),
+ resolver: {
+ resolve(specifier) {
+ // Strip `root:` prefix off specifier and resolve it as an absolute path
+ // in the test data root.
+ return path.join(root, specifier.slice('root:'.length));
+ },
+ },
+ });
+ const code = buffer.toString().trim();
+
+ const expected = `
+.baz {
+ color: #00f;
+}
+
+.bar {
+ color: green;
+}
+
+.foo {
+ color: red;
+}
+ `.trim();
+ if (code !== expected)
+ throw new Error(
+ `\`testOnlyCustomResolve()\` failed. Expected:\n${expected}\n\nGot:\n${code}`,
+ );
+});
+
+test('async read', async () => {
+ const root = path.join('tests', 'testdata');
+ const {code: buffer} = await bundleAsync({
+ filename: path.join(root, 'foo.css'),
+ resolver: {
+ async read(file) {
+ return await fs.promises.readFile(file, 'utf8');
+ },
+ resolve(specifier) {
+ // Strip `root:` prefix off specifier and resolve it as an absolute path
+ // in the test data root.
+ return path.join(root, specifier.slice('root:'.length));
+ },
+ },
+ });
+ const code = buffer.toString().trim();
+
+ const expected = `
+.baz {
+ color: #00f;
+}
+
+.bar {
+ color: green;
+}
+
+.foo {
+ color: red;
+}
+ `.trim();
+ if (code !== expected)
+ throw new Error(
+ `\`testAsyncRead()\` failed. Expected:\n${expected}\n\nGot:\n${code}`,
+ );
+});
+
+test('read throw', async () => {
+ let error: BundleAsyncError | undefined = undefined;
+ try {
+ await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ read(file) {
+ throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(`Oh noes! Failed to read \`foo.css\`.`);
+ expect(error.loc).toBeUndefined(); // error occurred when reading initial file, no location info available.
+});
+
+test('async read throw', async () => {
+ let error: BundleAsyncError | undefined = undefined;
+ try {
+ await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ async read(file) {
+ throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(`Oh noes! Failed to read \`foo.css\`.`);
+ expect(error.loc).toBeUndefined(); // error occurred when reading initial file, no location info available.
+});
+
+test('read throw with location info', async () => {
+ let error: BundleAsyncError | undefined = undefined;
+ try {
+ await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ read(file) {
+ if (file === 'foo.css') {
+ return '@import "bar.css"';
+ }
+ throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(`Oh noes! Failed to read \`bar.css\`.`);
+ expect(error.fileName).toBe('foo.css');
+ expect(error.loc).toEqual({
+ line: 1,
+ column: 1,
+ });
+});
+
+test('async read throw with location info', async () => {
+ let error: BundleAsyncError | undefined = undefined;
+ try {
+ await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ async read(file) {
+ if (file === 'foo.css') {
+ return '@import "bar.css"';
+ }
+ throw new Error(`Oh noes! Failed to read \`${file}\`.`);
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testReadThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(`Oh noes! Failed to read \`bar.css\`.`);
+ expect(error.fileName).toBe('foo.css');
+ expect(error.loc).toEqual({
+ line: 1,
+ column: 1,
+ });
+});
+
+test('resolve throw', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'tests/testdata/foo.css',
+ resolver: {
+ resolve(specifier, originatingFile) {
+ throw new Error(
+ `Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`,
+ );
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(
+ `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`,
+ );
+ expect(error.fileName).toBe('tests/testdata/foo.css');
+ expect(error.loc).toEqual({
+ line: 1,
+ column: 1,
+ });
+});
+
+test('async resolve throw', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'tests/testdata/foo.css',
+ resolver: {
+ async resolve(specifier, originatingFile) {
+ throw new Error(
+ `Oh noes! Failed to resolve \`${specifier}\` from \`${originatingFile}\`.`,
+ );
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testResolveThrow()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe(
+ `Oh noes! Failed to resolve \`root:hello/world.css\` from \`tests/testdata/foo.css\`.`,
+ );
+ expect(error.fileName).toBe('tests/testdata/foo.css');
+ expect(error.loc).toEqual({
+ line: 1,
+ column: 1,
+ });
+});
+
+test('read return non-string', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'foo.css',
+ resolver: {
+ // @ts-expect-error Test is for unexpected value
+ read() {
+ return 1234; // Returns a non-string value.
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testReadReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe('expect String, got: Number');
+});
+
+test('resolve return non-string', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'tests/testdata/foo.css',
+ resolver: {
+ // @ts-expect-error Test is for unexpected value
+ resolve() {
+ return 1234; // Returns a non-string value.
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ if (!error)
+ throw new Error(
+ `\`testResolveReturnNonString()\` failed. Expected \`bundleAsync()\` to throw, but it did not.`,
+ );
+ expect(error.message).toBe('expect String, got: Number');
+ expect(error.fileName).toBe('tests/testdata/foo.css');
+ expect(error.loc).toEqual({
+ line: 1,
+ column: 1,
+ });
+});
+
+test('should throw with location info on syntax errors', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'tests/testdata/foo.css',
+ resolver: {
+ read() {
+ return '.foo';
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+ expect(error!.message).toBe(`Unexpected end of input`);
+ expect(error!.fileName).toBe('tests/testdata/foo.css');
+ expect(error!.loc).toEqual({
+ line: 1,
+ column: 5,
+ });
+});
+
+test('should support throwing in visitors', async () => {
+ let error: BundleAsyncError | undefined;
+ try {
+ await bundleAsync({
+ filename: 'tests/testdata/a.css',
+ visitor: {
+ Rule() {
+ throw new Error('Some error');
+ },
+ },
+ });
+ } catch (err) {
+ error = err;
+ }
+
+ expect(error!.message).toBe('Some error');
+});
diff --git a/node/test/composeVisitors.test.mjs b/node/test/composeVisitors.test.mjs
deleted file mode 100644
index 7718ec06..00000000
--- a/node/test/composeVisitors.test.mjs
+++ /dev/null
@@ -1,803 +0,0 @@
-// @ts-check
-
-import { test } from 'uvu';
-import * as assert from 'uvu/assert';
-import {webcrypto as crypto} from 'node:crypto';
-
-let transform, composeVisitors;
-if (process.env.TEST_WASM === 'node') {
- ({transform, composeVisitors} = await import('../../wasm/wasm-node.mjs'));
-} else if (process.env.TEST_WASM === 'browser') {
- // Define crypto globally for old node.
- // @ts-ignore
- globalThis.crypto ??= crypto;
- let wasm = await import('../../wasm/index.mjs');
- await wasm.default();
- ({transform, composeVisitors} = wasm);
-} else {
- ({transform, composeVisitors} = await import('../index.mjs'));
-}
-
-test('different types', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: 16px;
- color: red;
- }
- `),
- visitor: composeVisitors([
- {
- Length(l) {
- if (l.unit === 'px') {
- return {
- unit: 'rem',
- value: l.value / 16
- }
- }
- }
- },
- {
- Color(c) {
- if (c.type === 'rgb') {
- return {
- type: 'rgb',
- r: c.g,
- g: c.r,
- b: c.b,
- alpha: c.alpha
- };
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{color:#0f0;width:1rem}');
-});
-
-test('simple matching types', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: 16px;
- }
- `),
- visitor: composeVisitors([
- {
- Length(l) {
- return {
- unit: l.unit,
- value: l.value * 2
- };
- }
- },
- {
- Length(l) {
- if (l.unit === 'px') {
- return {
- unit: 'rem',
- value: l.value / 16
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:2rem}');
-});
-
-test('different properties', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- size: 16px;
- bg: #ff0;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration: {
- custom: {
- size(v) {
- return [
- { property: 'unparsed', value: { propertyId: { property: 'width' }, value: v.value } },
- { property: 'unparsed', value: { propertyId: { property: 'height' }, value: v.value } }
- ];
- }
- }
- }
- },
- {
- Declaration: {
- custom: {
- bg(v) {
- if (v.value[0].type === 'color') {
- return { property: 'background-color', value: v.value[0].value };
- }
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:16px;height:16px;background-color:#ff0}');
-});
-
-test('composed properties', () => {
- /** @type {import('../index').Visitor[]} */
- let visitors = [
- {
- Declaration: {
- custom: {
- size(v) {
- if (v.value[0].type === 'length') {
- return [
- { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: v.value[0].value } } },
- { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: v.value[0].value } } },
- ];
- }
- }
- }
- }
- },
- {
- Declaration: {
- width() {
- return [];
- }
- }
- }
- ];
-
- // Check that it works in any order.
- for (let i = 0; i < 2; i++) {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- size: 16px;
- }
- `),
- visitor: composeVisitors(visitors)
- });
-
- assert.equal(res.code.toString(), '.foo{height:16px}');
- visitors.reverse();
- }
-});
-
-test('same properties', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- color: red;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration: {
- color(v) {
- if (v.property === 'color' && v.value.type === 'rgb') {
- return {
- property: 'color',
- value: {
- type: 'rgb',
- r: v.value.g,
- g: v.value.r,
- b: v.value.b,
- alpha: v.value.alpha
- }
- };
- }
- }
- }
- },
- {
- Declaration: {
- color(v) {
- if (v.property === 'color' && v.value.type === 'rgb' && v.value.g > 0) {
- v.value.alpha /= 2;
- }
- return v;
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{color:#00ff0080}');
-});
-
-test('properties plus values', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- size: test;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration: {
- custom: {
- size() {
- return [
- { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
- { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
- ];
- }
- }
- }
- },
- {
- Length(l) {
- if (l.unit === 'px') {
- return {
- unit: 'rem',
- value: l.value / 16
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:2rem;height:2rem}');
-});
-
-test('unparsed properties', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: test;
- }
- .bar {
- width: 16px;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration: {
- width(v) {
- if (v.property === 'unparsed') {
- return [
- { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
- { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } },
- ];
- }
- }
- }
- },
- {
- Length(l) {
- if (l.unit === 'px') {
- return {
- unit: 'rem',
- value: l.value / 16
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:2rem;height:2rem}.bar{width:1rem}');
-});
-
-test('returning unparsed properties', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: test;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration: {
- width(v) {
- if (v.property === 'unparsed' && v.value.value[0].type === 'token' && v.value.value[0].value.type === 'ident') {
- return {
- property: 'unparsed',
- value: {
- propertyId: { property: 'width' },
- value: [{
- type: 'var',
- value: {
- name: {
- ident: '--' + v.value.value[0].value.value
- }
- }
- }]
- }
- }
- }
- }
- }
- },
- {
- Declaration: {
- width(v) {
- if (v.property === 'unparsed') {
- return {
- property: 'unparsed',
- value: {
- propertyId: { property: 'width' },
- value: [{
- type: 'function',
- value: {
- name: 'calc',
- arguments: v.value.value
- }
- }]
- }
- }
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:calc(var(--test))}');
-});
-
-test('all property handlers', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: test;
- height: test;
- }
- `),
- visitor: composeVisitors([
- {
- Declaration(decl) {
- if (decl.property === 'unparsed' && decl.value.propertyId.property === 'width') {
- return { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
- }
- }
- },
- {
- Declaration(decl) {
- if (decl.property === 'unparsed' && decl.value.propertyId.property === 'height') {
- return { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:32px;height:32px}');
-});
-
-test('all property handlers (exit)', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: test;
- height: test;
- }
- `),
- visitor: composeVisitors([
- {
- DeclarationExit(decl) {
- if (decl.property === 'unparsed' && decl.value.propertyId.property === 'width') {
- return { property: 'width', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
- }
- }
- },
- {
- DeclarationExit(decl) {
- if (decl.property === 'unparsed' && decl.value.propertyId.property === 'height') {
- return { property: 'height', value: { type: 'length-percentage', value: { type: 'dimension', value: { unit: 'px', value: 32 } } } };
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:32px;height:32px}');
-});
-
-test('tokens and functions', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- width: f3(f2(f1(test)));
- }
- `),
- visitor: composeVisitors([
- {
- FunctionExit: {
- f1(f) {
- if (f.arguments.length === 1 && f.arguments[0].type === 'token' && f.arguments[0].value.type === 'ident') {
- return {
- type: 'length',
- value: {
- unit: 'px',
- value: 32
- }
- }
- }
- }
- }
- },
- {
- FunctionExit(f) {
- return f.arguments[0];
- }
- },
- {
- Length(l) {
- if (l.unit === 'px') {
- return {
- unit: 'rem',
- value: l.value / 16
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.foo{width:2rem}');
-});
-
-test('unknown rules', () => {
- let declared = new Map();
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @test #056ef0;
-
- .menu_link {
- background: @blue;
- }
- `),
- visitor: composeVisitors([
- {
- Rule: {
- unknown: {
- test(rule) {
- rule.name = 'blue';
- return {
- type: 'unknown',
- value: rule
- };
- }
- }
- }
- },
- {
- Rule: {
- unknown(rule) {
- declared.set(rule.name, rule.prelude);
- return [];
- }
- },
- Token: {
- 'at-keyword'(token) {
- if (declared.has(token.value)) {
- return declared.get(token.value);
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.menu_link{background:#056ef0}');
-});
-
-test('custom at rules', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @testA;
- @testB;
- `),
- customAtRules: {
- testA: {},
- testB: {}
- },
- visitor: composeVisitors([
- {
- Rule: {
- custom: {
- testA(rule) {
- return {
- type: 'style',
- value: {
- loc: rule.loc,
- selectors: [
- [{ type: 'class', name: 'testA' }]
- ],
- declarations: {
- declarations: [
- {
- property: 'color',
- value: {
- type: 'rgb',
- r: 0xff,
- g: 0x00,
- b: 0x00,
- alpha: 1,
- }
- }
- ]
- }
- }
- };
- }
- }
- }
- },
- {
- Rule: {
- custom: {
- testB(rule) {
- return {
- type: 'style',
- value: {
- loc: rule.loc,
- selectors: [
- [{ type: 'class', name: 'testB' }]
- ],
- declarations: {
- declarations: [
- {
- property: 'color',
- value: {
- type: 'rgb',
- r: 0x00,
- g: 0xff,
- b: 0x00,
- alpha: 1,
- }
- }
- ]
- }
- }
- };
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.testA{color:red}.testB{color:#0f0}');
-});
-
-test('known rules', () => {
- let declared = new Map();
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .test:focus-visible {
- margin-left: 20px;
- margin-right: @margin-left;
- }
- `),
- targets: {
- safari: 14 << 16
- },
- visitor: composeVisitors([
- {
- Rule: {
- style(rule) {
- let valuesByProperty = new Map();
- for (let decl of rule.value.declarations.declarations) {
- /** @type string */
- let name = decl.property;
- if (decl.property === 'unparsed') {
- name = decl.value.propertyId.property;
- }
- valuesByProperty.set(name, decl);
- }
-
- rule.value.declarations.declarations = rule.value.declarations.declarations.map(decl => {
- // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
- if (decl.property === 'unparsed' && decl.value.value.length === 1) {
- let token = decl.value.value[0];
- if (token.type === 'token' && token.value.type === 'at-keyword' && valuesByProperty.has(token.value.value)) {
- let v = valuesByProperty.get(token.value.value);
- return {
- /** @type any */
- property: decl.value.propertyId.property,
- value: v.value
- };
- }
- }
- return decl;
- });
-
- return rule;
- }
- }
- },
- {
- Rule: {
- style(rule) {
- let clone = null;
- for (let selector of rule.value.selectors) {
- for (let [i, component] of selector.entries()) {
- if (component.type === 'pseudo-class' && component.kind === 'focus-visible') {
- if (clone == null) {
- clone = [...rule.value.selectors.map(s => [...s])];
- }
-
- selector[i] = { type: 'class', name: 'focus-visible' };
- }
- }
- }
-
- if (clone) {
- return [rule, { type: 'style', value: { ...rule.value, selectors: clone } }];
- }
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '.test.focus-visible{margin-left:20px;margin-right:20px}.test:focus-visible{margin-left:20px;margin-right:20px}');
-});
-
-test('environment variables', () => {
- /** @type {Record} */
- let tokens = {
- '--branding-small': {
- type: 'length',
- value: {
- unit: 'px',
- value: 600
- }
- },
- '--branding-padding': {
- type: 'length',
- value: {
- unit: 'px',
- value: 20
- }
- }
- };
-
- let res = transform({
- filename: 'test.css',
- minify: true,
- errorRecovery: true,
- code: Buffer.from(`
- @media (max-width: env(--branding-small)) {
- body {
- padding: env(--branding-padding);
- }
- }
- `),
- visitor: composeVisitors([
- {
- EnvironmentVariable: {
- '--branding-small': () => tokens['--branding-small']
- }
- },
- {
- EnvironmentVariable: {
- '--branding-padding': () => tokens['--branding-padding']
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
-});
-
-test('variables', () => {
- /** @type {Record} */
- let tokens = {
- '--branding-small': {
- type: 'length',
- value: {
- unit: 'px',
- value: 600
- }
- },
- '--branding-padding': {
- type: 'length',
- value: {
- unit: 'px',
- value: 20
- }
- }
- };
-
- let res = transform({
- filename: 'test.css',
- minify: true,
- errorRecovery: true,
- code: Buffer.from(`
- body {
- padding: var(--branding-padding);
- width: var(--branding-small);
- }
- `),
- visitor: composeVisitors([
- {
- Variable(v) {
- if (v.name.ident === '--branding-small') {
- return tokens['--branding-small'];
- }
- }
- },
- {
- Variable(v) {
- if (v.name.ident === '--branding-padding') {
- return tokens['--branding-padding'];
- }
- }
- }
- ])
- });
-
- assert.equal(res.code.toString(), 'body{padding:20px;width:600px}');
-});
-
-test('StyleSheet', () => {
- let styleSheetCalledCount = 0;
- let styleSheetExitCalledCount = 0;
- transform({
- filename: 'test.css',
- code: Buffer.from(`
- body {
- color: blue;
- }
- `),
- visitor: composeVisitors([
- {
- StyleSheet() {
- styleSheetCalledCount++
- },
- StyleSheetExit() {
- styleSheetExitCalledCount++
- }
- },
- {
- StyleSheet() {
- styleSheetCalledCount++
- },
- StyleSheetExit() {
- styleSheetExitCalledCount++
- }
- }
- ])
- });
- assert.equal(styleSheetCalledCount, 2);
- assert.equal(styleSheetExitCalledCount, 2);
-});
-
-test.run();
diff --git a/node/test/composeVisitors.test.ts b/node/test/composeVisitors.test.ts
new file mode 100644
index 00000000..2e3d9070
--- /dev/null
+++ b/node/test/composeVisitors.test.ts
@@ -0,0 +1,917 @@
+///
+
+import {beforeAll, expect, test} from 'vitest';
+import type {
+ transform as Transform,
+ composeVisitors as ComposeVisitors,
+ TokenOrValue,
+ Visitor,
+ SelectorList,
+} from '../index.js';
+
+let transform: typeof Transform;
+let composeVisitors: typeof ComposeVisitors;
+
+beforeAll(async () => {
+ if (import.meta.env.TEST_WASM === 'node') {
+ ({transform, composeVisitors} = await import('../../wasm/wasm-node.mjs'));
+ } else if (import.meta.env.TEST_WASM === 'browser') {
+ let wasm = await import('../../wasm/index.mjs');
+ await wasm.default();
+ ({transform, composeVisitors} = wasm);
+ } else {
+ ({transform, composeVisitors} = await import('../index.mjs'));
+ }
+});
+
+test('different types', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: 16px;
+ color: red;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Length(l) {
+ if (l.unit === 'px') {
+ return {
+ unit: 'rem',
+ value: l.value / 16,
+ };
+ }
+ },
+ },
+ {
+ Color(c) {
+ if (typeof c === 'object' && c.type === 'rgb') {
+ return {
+ type: 'rgb',
+ r: c.g,
+ g: c.r,
+ b: c.b,
+ alpha: c.alpha,
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{color:#0f0;width:1rem}');
+});
+
+test('simple matching types', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: 16px;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Length(l) {
+ return {
+ unit: l.unit,
+ value: l.value * 2,
+ };
+ },
+ },
+ {
+ Length(l) {
+ if (l.unit === 'px') {
+ return {
+ unit: 'rem',
+ value: l.value / 16,
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:2rem}');
+});
+
+test('different properties', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ size: 16px;
+ bg: #ff0;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration: {
+ custom: {
+ size(v) {
+ return [
+ {
+ property: 'unparsed',
+ value: {propertyId: {property: 'width'}, value: v.value},
+ },
+ {
+ property: 'unparsed',
+ value: {propertyId: {property: 'height'}, value: v.value},
+ },
+ ];
+ },
+ },
+ },
+ },
+ {
+ Declaration: {
+ custom: {
+ bg(v) {
+ if (v.value[0].type === 'color') {
+ return {property: 'background-color', value: v.value[0].value};
+ }
+ },
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe(
+ '.foo{width:16px;height:16px;background-color:#ff0}',
+ );
+});
+
+test('composed properties', () => {
+ let visitors: Visitor[] = [
+ {
+ Declaration: {
+ custom: {
+ size(v) {
+ if (v.value[0].type === 'length') {
+ return [
+ {
+ property: 'width',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: v.value[0].value},
+ },
+ },
+ {
+ property: 'height',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: v.value[0].value},
+ },
+ },
+ ];
+ }
+ },
+ },
+ },
+ },
+ {
+ Declaration: {
+ width() {
+ return [];
+ },
+ },
+ },
+ ];
+
+ // Check that it works in any order.
+ for (let i = 0; i < 2; i++) {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ size: 16px;
+ }
+ `),
+ visitor: composeVisitors(visitors),
+ });
+
+ expect(res.code.toString()).toBe('.foo{height:16px}');
+ visitors.reverse();
+ }
+});
+
+test('same properties', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ color: red;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration: {
+ color(v) {
+ if (
+ v.property === 'color' &&
+ typeof v.value === 'object' &&
+ v.value.type === 'rgb'
+ ) {
+ return {
+ property: 'color',
+ value: {
+ type: 'rgb',
+ r: v.value.g,
+ g: v.value.r,
+ b: v.value.b,
+ alpha: v.value.alpha,
+ },
+ };
+ }
+ },
+ },
+ },
+ {
+ Declaration: {
+ color(v) {
+ if (
+ v.property === 'color' &&
+ typeof v.value === 'object' &&
+ v.value.type === 'rgb' &&
+ v.value.g > 0
+ ) {
+ v.value.alpha /= 2;
+ }
+ return v;
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{color:#00ff0080}');
+});
+
+test('properties plus values', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ size: test;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration: {
+ custom: {
+ size() {
+ return [
+ {
+ property: 'width',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ },
+ {
+ property: 'height',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ },
+ ];
+ },
+ },
+ },
+ },
+ {
+ Length(l) {
+ if (l.unit === 'px') {
+ return {
+ unit: 'rem',
+ value: l.value / 16,
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:2rem;height:2rem}');
+});
+
+test('unparsed properties', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: test;
+ }
+ .bar {
+ width: 16px;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration: {
+ width(v) {
+ if (v.property === 'unparsed') {
+ return [
+ {
+ property: 'width',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ },
+ {
+ property: 'height',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ },
+ ];
+ }
+ },
+ },
+ },
+ {
+ Length(l) {
+ if (l.unit === 'px') {
+ return {
+ unit: 'rem',
+ value: l.value / 16,
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe(
+ '.foo{width:2rem;height:2rem}.bar{width:1rem}',
+ );
+});
+
+test('returning unparsed properties', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: test;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration: {
+ width(v) {
+ if (
+ v.property === 'unparsed' &&
+ v.value.value[0].type === 'token' &&
+ v.value.value[0].value.type === 'ident'
+ ) {
+ return {
+ property: 'unparsed',
+ value: {
+ propertyId: {property: 'width'},
+ value: [
+ {
+ type: 'var',
+ value: {
+ name: {
+ ident: '--' + v.value.value[0].value.value,
+ },
+ },
+ },
+ ],
+ },
+ };
+ }
+ },
+ },
+ },
+ {
+ Declaration: {
+ width(v) {
+ if (v.property === 'unparsed') {
+ return {
+ property: 'unparsed',
+ value: {
+ propertyId: {property: 'width'},
+ value: [
+ {
+ type: 'function',
+ value: {
+ name: 'calc',
+ arguments: v.value.value,
+ },
+ },
+ ],
+ },
+ };
+ }
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:calc(var(--test))}');
+});
+
+test('all property handlers', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: test;
+ height: test;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Declaration(decl) {
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.propertyId.property === 'width'
+ ) {
+ return {
+ property: 'width',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ };
+ }
+ },
+ },
+ {
+ Declaration(decl) {
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.propertyId.property === 'height'
+ ) {
+ return {
+ property: 'height',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:32px;height:32px}');
+});
+
+test('all property handlers (exit)', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: test;
+ height: test;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ DeclarationExit(decl) {
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.propertyId.property === 'width'
+ ) {
+ return {
+ property: 'width',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ };
+ }
+ },
+ },
+ {
+ DeclarationExit(decl) {
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.propertyId.property === 'height'
+ ) {
+ return {
+ property: 'height',
+ value: {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: {unit: 'px', value: 32}},
+ },
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:32px;height:32px}');
+});
+
+test('tokens and functions', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ width: f3(f2(f1(test)));
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ FunctionExit: {
+ f1(f) {
+ if (
+ f.arguments.length === 1 &&
+ f.arguments[0].type === 'token' &&
+ f.arguments[0].value.type === 'ident'
+ ) {
+ return {
+ type: 'length',
+ value: {
+ unit: 'px',
+ value: 32,
+ },
+ };
+ }
+ },
+ },
+ },
+ {
+ FunctionExit(f) {
+ return f.arguments[0];
+ },
+ },
+ {
+ Length(l) {
+ if (l.unit === 'px') {
+ return {
+ unit: 'rem',
+ value: l.value / 16,
+ };
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:2rem}');
+});
+
+test('unknown rules', () => {
+ let declared = new Map();
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @test #056ef0;
+
+ .menu_link {
+ background: @blue;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Rule: {
+ unknown: {
+ test(rule) {
+ rule.name = 'blue';
+ return {
+ type: 'unknown',
+ value: rule,
+ };
+ },
+ },
+ },
+ },
+ {
+ Rule: {
+ unknown(rule) {
+ declared.set(rule.name, rule.prelude);
+ return [];
+ },
+ },
+ Token: {
+ 'at-keyword'(token) {
+ if (declared.has(token.value)) {
+ return declared.get(token.value);
+ }
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.menu_link{background:#056ef0}');
+});
+
+test('custom at rules', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @testA;
+ @testB;
+ `),
+ customAtRules: {
+ testA: {},
+ testB: {},
+ },
+ visitor: composeVisitors([
+ {
+ Rule: {
+ custom: {
+ testA(rule) {
+ return {
+ type: 'style',
+ value: {
+ loc: rule.loc,
+ selectors: [[{type: 'class', name: 'testA'}]],
+ declarations: {
+ declarations: [
+ {
+ property: 'color',
+ value: {
+ type: 'rgb',
+ r: 0xff,
+ g: 0x00,
+ b: 0x00,
+ alpha: 1,
+ },
+ },
+ ],
+ },
+ },
+ };
+ },
+ },
+ },
+ },
+ {
+ Rule: {
+ custom: {
+ testB(rule) {
+ return {
+ type: 'style',
+ value: {
+ loc: rule.loc,
+ selectors: [[{type: 'class', name: 'testB'}]],
+ declarations: {
+ declarations: [
+ {
+ property: 'color',
+ value: {
+ type: 'rgb',
+ r: 0x00,
+ g: 0xff,
+ b: 0x00,
+ alpha: 1,
+ },
+ },
+ ],
+ },
+ },
+ };
+ },
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('.testA{color:red}.testB{color:#0f0}');
+});
+
+test('known rules', () => {
+ let declared = new Map();
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .test:focus-visible {
+ margin-left: 20px;
+ margin-right: @margin-left;
+ }
+ `),
+ targets: {
+ safari: 14 << 16,
+ },
+ visitor: composeVisitors([
+ {
+ Rule: {
+ style(rule) {
+ let valuesByProperty = new Map();
+ for (let decl of rule.value.declarations.declarations) {
+ let name = decl.property;
+ if (decl.property === 'unparsed') {
+ name = decl.value.propertyId.property as typeof name;
+ }
+ valuesByProperty.set(name, decl);
+ }
+
+ rule.value.declarations.declarations =
+ rule.value.declarations.declarations.map((decl) => {
+ // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.value.length === 1
+ ) {
+ let token = decl.value.value[0];
+ if (
+ token.type === 'token' &&
+ token.value.type === 'at-keyword' &&
+ valuesByProperty.has(token.value.value)
+ ) {
+ let v = valuesByProperty.get(token.value.value);
+ return {
+ property: decl.value.propertyId.property,
+ value: v.value,
+ };
+ }
+ }
+ return decl;
+ }) as typeof rule.value.declarations.declarations;
+
+ return rule;
+ },
+ },
+ },
+ {
+ Rule: {
+ style(rule) {
+ let clone: SelectorList | null = null;
+ for (let selector of rule.value.selectors) {
+ for (let [i, component] of selector.entries()) {
+ if (
+ component.type === 'pseudo-class' &&
+ component.kind === 'focus-visible'
+ ) {
+ if (clone == null) {
+ clone = [...rule.value.selectors.map((s) => [...s])];
+ }
+
+ selector[i] = {type: 'class', name: 'focus-visible'};
+ }
+ }
+ }
+
+ if (clone) {
+ return [
+ rule,
+ {type: 'style', value: {...rule.value, selectors: clone}},
+ ];
+ }
+ },
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe(
+ '.test.focus-visible{margin-left:20px;margin-right:20px}.test:focus-visible{margin-left:20px;margin-right:20px}',
+ );
+});
+
+test('environment variables', () => {
+ let tokens: Record = {
+ '--branding-small': {
+ type: 'length',
+ value: {
+ unit: 'px',
+ value: 600,
+ },
+ },
+ '--branding-padding': {
+ type: 'length',
+ value: {
+ unit: 'px',
+ value: 20,
+ },
+ },
+ };
+
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ errorRecovery: true,
+ code: Buffer.from(`
+ @media (max-width: env(--branding-small)) {
+ body {
+ padding: env(--branding-padding);
+ }
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ EnvironmentVariable: {
+ '--branding-small': () => tokens['--branding-small'],
+ },
+ },
+ {
+ EnvironmentVariable: {
+ '--branding-padding': () => tokens['--branding-padding'],
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('@media (width<=600px){body{padding:20px}}');
+});
+
+test('variables', () => {
+ let tokens: Record = {
+ '--branding-small': {
+ type: 'length',
+ value: {
+ unit: 'px',
+ value: 600,
+ },
+ },
+ '--branding-padding': {
+ type: 'length',
+ value: {
+ unit: 'px',
+ value: 20,
+ },
+ },
+ };
+
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ errorRecovery: true,
+ code: Buffer.from(`
+ body {
+ padding: var(--branding-padding);
+ width: var(--branding-small);
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ Variable(v) {
+ if (v.name.ident === '--branding-small') {
+ return tokens['--branding-small'];
+ }
+ },
+ },
+ {
+ Variable(v) {
+ if (v.name.ident === '--branding-padding') {
+ return tokens['--branding-padding'];
+ }
+ },
+ },
+ ]),
+ });
+
+ expect(res.code.toString()).toBe('body{padding:20px;width:600px}');
+});
+
+test('StyleSheet', () => {
+ let styleSheetCalledCount = 0;
+ let styleSheetExitCalledCount = 0;
+ transform({
+ filename: 'test.css',
+ code: Buffer.from(`
+ body {
+ color: blue;
+ }
+ `),
+ visitor: composeVisitors([
+ {
+ StyleSheet() {
+ styleSheetCalledCount++;
+ },
+ StyleSheetExit() {
+ styleSheetExitCalledCount++;
+ },
+ },
+ {
+ StyleSheet() {
+ styleSheetCalledCount++;
+ },
+ StyleSheetExit() {
+ styleSheetExitCalledCount++;
+ },
+ },
+ ]),
+ });
+ expect(styleSheetCalledCount).toBe(2);
+ expect(styleSheetExitCalledCount).toBe(2);
+});
diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs
deleted file mode 100644
index e53f65ad..00000000
--- a/node/test/customAtRules.mjs
+++ /dev/null
@@ -1,317 +0,0 @@
-// @ts-check
-
-import { test } from 'uvu';
-import * as assert from 'uvu/assert';
-import fs from 'fs';
-import {webcrypto as crypto} from 'node:crypto';
-
-let bundle, transform;
-if (process.env.TEST_WASM === 'node') {
- ({ bundle, transform } = await import('../../wasm/wasm-node.mjs'));
-} else if (process.env.TEST_WASM === 'browser') {
- // Define crypto globally for old node.
- // @ts-ignore
- globalThis.crypto ??= crypto;
- let wasm = await import('../../wasm/index.mjs');
- await wasm.default();
- transform = wasm.transform;
- bundle = function(options) {
- return wasm.bundle({
- ...options,
- resolver: {
- read: (filePath) => fs.readFileSync(filePath, 'utf8')
- }
- });
- }
-} else {
- ({bundle, transform} = await import('../index.mjs'));
-}
-
-test('declaration list', () => {
- let definitions = new Map();
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @theme spacing {
- foo: 16px;
- bar: 32px;
- }
-
- .foo {
- width: theme('spacing.foo');
- }
- `),
- customAtRules: {
- theme: {
- prelude: '',
- body: 'declaration-list'
- }
- },
- visitor: {
- Rule: {
- custom: {
- theme(rule) {
- for (let decl of rule.body.value.declarations) {
- if (decl.property === 'custom') {
- definitions.set(rule.prelude.value + '.' + decl.value.name, decl.value.value);
- }
- }
- return [];
- }
- }
- },
- Function: {
- theme(f) {
- if (f.arguments[0].type === 'token' && f.arguments[0].value.type === 'string') {
- return definitions.get(f.arguments[0].value.value);
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '.foo{width:16px}');
-});
-
-test('mixin', () => {
- let mixins = new Map();
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @mixin color {
- color: red;
-
- &.bar {
- color: yellow;
- }
- }
-
- .foo {
- @apply color;
- }
- `),
- targets: { chrome: 100 << 16 },
- customAtRules: {
- mixin: {
- prelude: '',
- body: 'style-block'
- },
- apply: {
- prelude: ''
- }
- },
- visitor: {
- Rule: {
- custom: {
- mixin(rule) {
- mixins.set(rule.prelude.value, rule.body.value);
- return [];
- },
- apply(rule) {
- return mixins.get(rule.prelude.value);
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
-});
-
-test('rule list', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @breakpoint 1024px {
- .foo { color: yellow; }
- }
- `),
- customAtRules: {
- breakpoint: {
- prelude: '',
- body: 'rule-list'
- }
- },
- visitor: {
- Rule: {
- custom: {
- breakpoint(rule) {
- return {
- type: 'media',
- value: {
- query: {
- mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
- },
- rules: rule.body.value,
- loc: rule.loc
- }
- }
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}}');
-});
-
-
-test('style block', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- .foo {
- @breakpoint 1024px {
- color: yellow;
-
- &.bar {
- color: red;
- }
- }
- }
- `),
- targets: {
- chrome: 105 << 16
- },
- customAtRules: {
- breakpoint: {
- prelude: '',
- body: 'style-block'
- }
- },
- visitor: {
- Rule: {
- custom: {
- breakpoint(rule) {
- return {
- type: 'media',
- value: {
- query: {
- mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
- },
- rules: rule.body.value,
- loc: rule.loc
- }
- }
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}');
-});
-
-test('style block top level', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @test {
- .foo {
- background: black;
- }
- }
- `),
- customAtRules: {
- test: {
- body: 'style-block'
- }
- }
- });
-
- assert.equal(res.code.toString(), '@test{.foo{background:#000}}');
-});
-
-test('multiple', () => {
- let res = transform({
- filename: 'test.css',
- minify: true,
- code: Buffer.from(`
- @breakpoint 1024px {
- @theme spacing {
- foo: 16px;
- bar: 32px;
- }
- }
- `),
- customAtRules: {
- breakpoint: {
- prelude: '',
- body: 'rule-list'
- },
- theme: {
- prelude: '',
- body: 'declaration-list'
- }
- },
- visitor: {
- Rule: {
- custom(rule) {
- if (rule.name === 'breakpoint') {
- return {
- type: 'media',
- value: {
- query: {
- mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }]
- },
- rules: rule.body.value,
- loc: rule.loc
- }
- }
- } else {
- return {
- type: 'style',
- value: {
- selectors: [[{ type: 'pseudo-class', kind: 'root' }]],
- declarations: rule.body.value,
- loc: rule.loc
- }
- }
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '@media (width<=1024px){:root{foo:16px;bar:32px}}');
-});
-
-test('bundler', () => {
- let mixins = new Map();
- let res = bundle({
- filename: 'tests/testdata/apply.css',
- minify: true,
- targets: { chrome: 100 << 16 },
- customAtRules: {
- mixin: {
- prelude: '',
- body: 'style-block'
- },
- apply: {
- prelude: ''
- }
- },
- visitor: {
- Rule: {
- custom: {
- mixin(rule) {
- mixins.set(rule.prelude.value, rule.body.value);
- return [];
- },
- apply(rule) {
- return mixins.get(rule.prelude.value);
- }
- }
- }
- }
- });
-
- assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
-});
-
-test.run();
diff --git a/node/test/customAtRules.test.ts b/node/test/customAtRules.test.ts
new file mode 100644
index 00000000..aa300f5e
--- /dev/null
+++ b/node/test/customAtRules.test.ts
@@ -0,0 +1,363 @@
+///
+
+import fs from 'node:fs';
+import {beforeAll, expect, test} from 'vitest';
+import type {bundle as Bundle, transform as Transform} from '../index.js';
+
+let bundle: typeof Bundle;
+let transform: typeof Transform;
+
+beforeAll(async () => {
+ if (import.meta.env.TEST_WASM === 'node') {
+ ({bundle, transform} = await import('../../wasm/wasm-node.mjs'));
+ } else if (import.meta.env.TEST_WASM === 'browser') {
+ let wasm = await import('../../wasm/index.mjs');
+ await wasm.default();
+ transform = wasm.transform;
+ bundle = function (options) {
+ return wasm.bundle({
+ ...options,
+ resolver: {
+ read: (filePath) => fs.readFileSync(filePath, 'utf8'),
+ },
+ });
+ };
+ } else {
+ ({bundle, transform} = await import('../index.mjs'));
+ }
+});
+
+test('declaration list', () => {
+ let definitions = new Map();
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @theme spacing {
+ foo: 16px;
+ bar: 32px;
+ }
+
+ .foo {
+ width: theme('spacing.foo');
+ }
+ `),
+ customAtRules: {
+ theme: {
+ prelude: '',
+ body: 'declaration-list',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom: {
+ theme(rule) {
+ for (let decl of rule.body.value.declarations) {
+ if (decl.property === 'custom') {
+ definitions.set(
+ rule.prelude.value + '.' + decl.value.name,
+ decl.value.value,
+ );
+ }
+ }
+ return [];
+ },
+ },
+ },
+ Function: {
+ theme(f) {
+ if (
+ f.arguments[0].type === 'token' &&
+ f.arguments[0].value.type === 'string'
+ ) {
+ return definitions.get(f.arguments[0].value.value);
+ }
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe('.foo{width:16px}');
+});
+
+test('mixin', () => {
+ let mixins = new Map();
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @mixin color {
+ color: red;
+
+ &.bar {
+ color: yellow;
+ }
+ }
+
+ .foo {
+ @apply color;
+ }
+ `),
+ targets: {chrome: 100 << 16},
+ customAtRules: {
+ mixin: {
+ prelude: '',
+ body: 'style-block',
+ },
+ apply: {
+ prelude: '',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom: {
+ mixin(rule) {
+ mixins.set(rule.prelude.value, rule.body.value);
+ return [];
+ },
+ apply(rule) {
+ return mixins.get(rule.prelude.value);
+ },
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe('.foo{color:red}.foo.bar{color:#ff0}');
+});
+
+test('rule list', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @breakpoint 1024px {
+ .foo { color: yellow; }
+ }
+ `),
+ customAtRules: {
+ breakpoint: {
+ prelude: '',
+ body: 'rule-list',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom: {
+ breakpoint(rule) {
+ return {
+ type: 'media',
+ value: {
+ query: {
+ mediaQueries: [
+ {
+ mediaType: 'all',
+ condition: {
+ type: 'feature',
+ value: {
+ type: 'range',
+ name: 'width',
+ operator: 'less-than-equal',
+ value: rule.prelude,
+ },
+ },
+ },
+ ],
+ },
+ rules: rule.body.value,
+ loc: rule.loc,
+ },
+ };
+ },
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe('@media (width<=1024px){.foo{color:#ff0}}');
+});
+
+test('style block', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ .foo {
+ @breakpoint 1024px {
+ color: yellow;
+
+ &.bar {
+ color: red;
+ }
+ }
+ }
+ `),
+ targets: {
+ chrome: 105 << 16,
+ },
+ customAtRules: {
+ breakpoint: {
+ prelude: '',
+ body: 'style-block',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom: {
+ breakpoint(rule) {
+ return {
+ type: 'media',
+ value: {
+ query: {
+ mediaQueries: [
+ {
+ mediaType: 'all',
+ condition: {
+ type: 'feature',
+ value: {
+ type: 'range',
+ name: 'width',
+ operator: 'less-than-equal',
+ value: rule.prelude,
+ },
+ },
+ },
+ ],
+ },
+ rules: rule.body.value,
+ loc: rule.loc,
+ },
+ };
+ },
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe(
+ '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}',
+ );
+});
+
+test('style block top level', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @test {
+ .foo {
+ background: black;
+ }
+ }
+ `),
+ customAtRules: {
+ test: {
+ body: 'style-block',
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe('@test{.foo{background:#000}}');
+});
+
+test('multiple', () => {
+ let res = transform({
+ filename: 'test.css',
+ minify: true,
+ code: Buffer.from(`
+ @breakpoint 1024px {
+ @theme spacing {
+ foo: 16px;
+ bar: 32px;
+ }
+ }
+ `),
+ customAtRules: {
+ breakpoint: {
+ prelude: '',
+ body: 'rule-list',
+ },
+ theme: {
+ prelude: '',
+ body: 'declaration-list',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom(rule) {
+ if (rule.name === 'breakpoint') {
+ return {
+ type: 'media',
+ value: {
+ query: {
+ mediaQueries: [
+ {
+ mediaType: 'all',
+ condition: {
+ type: 'feature',
+ value: {
+ type: 'range',
+ name: 'width',
+ operator: 'less-than-equal',
+ value: rule.prelude,
+ },
+ },
+ },
+ ],
+ },
+ rules: rule.body.value,
+ loc: rule.loc,
+ },
+ };
+ } else {
+ return {
+ type: 'style',
+ value: {
+ selectors: [[{type: 'pseudo-class', kind: 'root'}]],
+ declarations: rule.body.value,
+ loc: rule.loc,
+ },
+ };
+ }
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe(
+ '@media (width<=1024px){:root{foo:16px;bar:32px}}',
+ );
+});
+
+test('bundler', () => {
+ let mixins = new Map();
+ let res = bundle({
+ filename: 'tests/testdata/apply.css',
+ minify: true,
+ targets: {chrome: 100 << 16},
+ customAtRules: {
+ mixin: {
+ prelude: '',
+ body: 'style-block',
+ },
+ apply: {
+ prelude: '',
+ },
+ },
+ visitor: {
+ Rule: {
+ custom: {
+ mixin(rule) {
+ mixins.set(rule.prelude.value, rule.body.value);
+ return [];
+ },
+ apply(rule) {
+ return mixins.get(rule.prelude.value);
+ },
+ },
+ },
+ },
+ });
+
+ expect(res.code.toString()).toBe('.foo{color:red}.foo.bar{color:#ff0}');
+});
diff --git a/node/test/transform.test.mjs b/node/test/transform.test.mjs
deleted file mode 100644
index 1b56bbc1..00000000
--- a/node/test/transform.test.mjs
+++ /dev/null
@@ -1,71 +0,0 @@
-import { test } from 'uvu';
-import * as assert from 'uvu/assert';
-import {webcrypto as crypto} from 'node:crypto';
-
-let transform, Features;
-if (process.env.TEST_WASM === 'node') {
- ({transform, Features} = await import('../../wasm/wasm-node.mjs'));
-} else if (process.env.TEST_WASM === 'browser') {
- // Define crypto globally for old node.
- // @ts-ignore
- globalThis.crypto ??= crypto;
- let wasm = await import('../../wasm/index.mjs');
- await wasm.default();
- ({transform, Features} = wasm);
-} else {
- ({transform, Features} = await import('../index.mjs'));
-}
-
-test('can enable non-standard syntax', () => {
- let res = transform({
- filename: 'test.css',
- code: Buffer.from('.foo >>> .bar { color: red }'),
- nonStandard: {
- deepSelectorCombinator: true
- },
- minify: true
- });
-
- assert.equal(res.code.toString(), '.foo>>>.bar{color:red}');
-});
-
-test('can enable features without targets', () => {
- let res = transform({
- filename: 'test.css',
- code: Buffer.from('.foo { .bar { color: red }}'),
- minify: true,
- include: Features.Nesting
- });
-
- assert.equal(res.code.toString(), '.foo .bar{color:red}');
-});
-
-test('can disable features', () => {
- let res = transform({
- filename: 'test.css',
- code: Buffer.from('.foo { color: lch(50.998% 135.363 338) }'),
- minify: true,
- targets: {
- chrome: 80 << 16
- },
- exclude: Features.Colors
- });
-
- assert.equal(res.code.toString(), '.foo{color:lch(50.998% 135.363 338)}');
-});
-
-test('can disable prefixing', () => {
- let res = transform({
- filename: 'test.css',
- code: Buffer.from('.foo { user-select: none }'),
- minify: true,
- targets: {
- safari: 15 << 16
- },
- exclude: Features.VendorPrefixes
- });
-
- assert.equal(res.code.toString(), '.foo{user-select:none}');
-});
-
-test.run();
diff --git a/node/test/transform.test.ts b/node/test/transform.test.ts
new file mode 100644
index 00000000..5d29dce4
--- /dev/null
+++ b/node/test/transform.test.ts
@@ -0,0 +1,74 @@
+///
+
+import {beforeAll, expect, test} from 'vitest';
+import type {
+ Features as FeaturesType,
+ transform as Transform,
+} from '../index.js';
+
+let transform: typeof Transform;
+let Features: typeof FeaturesType;
+
+beforeAll(async () => {
+ if (import.meta.env.TEST_WASM === 'node') {
+ ({transform, Features} = await import('../../wasm/wasm-node.mjs'));
+ } else if (import.meta.env.TEST_WASM === 'browser') {
+ let wasm = await import('../../wasm/index.mjs');
+ await wasm.default();
+ ({transform, Features} = wasm);
+ } else {
+ ({transform, Features} = await import('../index.mjs'));
+ }
+});
+
+test('can enable non-standard syntax', () => {
+ let res = transform({
+ filename: 'test.css',
+ code: Buffer.from('.foo >>> .bar { color: red }'),
+ nonStandard: {
+ deepSelectorCombinator: true,
+ },
+ minify: true,
+ });
+
+ expect(res.code.toString()).toBe('.foo>>>.bar{color:red}');
+});
+
+test('can enable features without targets', () => {
+ let res = transform({
+ filename: 'test.css',
+ code: Buffer.from('.foo { .bar { color: red }}'),
+ minify: true,
+ include: Features.Nesting,
+ });
+
+ expect(res.code.toString()).toBe('.foo .bar{color:red}');
+});
+
+test('can disable features', () => {
+ let res = transform({
+ filename: 'test.css',
+ code: Buffer.from('.foo { color: lch(50.998% 135.363 338) }'),
+ minify: true,
+ targets: {
+ chrome: 80 << 16,
+ },
+ exclude: Features.Colors,
+ });
+
+ expect(res.code.toString()).toBe('.foo{color:lch(50.998% 135.363 338)}');
+});
+
+test('can disable prefixing', () => {
+ let res = transform({
+ filename: 'test.css',
+ code: Buffer.from('.foo { user-select: none }'),
+ minify: true,
+ targets: {
+ safari: 15 << 16,
+ },
+ exclude: Features.VendorPrefixes,
+ });
+
+ expect(res.code.toString()).toBe('.foo{user-select:none}');
+});
diff --git a/node/test/visitor.test.mjs b/node/test/visitor.test.ts
similarity index 57%
rename from node/test/visitor.test.mjs
rename to node/test/visitor.test.ts
index 3a42a696..8ec42379 100644
--- a/node/test/visitor.test.mjs
+++ b/node/test/visitor.test.ts
@@ -1,42 +1,59 @@
-// @ts-check
-
-import { test } from 'uvu';
-import * as assert from 'uvu/assert';
-import fs from 'fs';
-import {webcrypto as crypto} from 'node:crypto';
-
-let bundle, bundleAsync, transform, transformStyleAttribute;
-if (process.env.TEST_WASM === 'node') {
- ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../../wasm/wasm-node.mjs'));
-} else if (process.env.TEST_WASM === 'browser') {
- // Define crypto globally for old node.
- // @ts-ignore
- globalThis.crypto ??= crypto;
- let wasm = await import('../../wasm/index.mjs');
- await wasm.default();
- ({ transform, transformStyleAttribute } = wasm);
- bundle = function(options) {
- return wasm.bundle({
- ...options,
- resolver: {
- read: (filePath) => fs.readFileSync(filePath, 'utf8')
- }
- });
- }
-
- bundleAsync = function (options) {
- if (!options.resolver?.read) {
- options.resolver = {
- ...options.resolver,
- read: (filePath) => fs.readFileSync(filePath, 'utf8')
- };
- }
-
- return wasm.bundleAsync(options);
+///
+
+import fs from 'node:fs';
+import {beforeAll, expect, test} from 'vitest';
+import type {
+ bundle as Bundle,
+ bundleAsync as BundleAsync,
+ ReturnedRule,
+ Rule,
+ Selector,
+ SelectorList,
+ Size,
+ TokenOrValue,
+ transform as Transform,
+ transformStyleAttribute as TransformStyleAttribute,
+} from '../index.js';
+
+let bundle: typeof Bundle;
+let bundleAsync: typeof BundleAsync;
+let transform: typeof Transform;
+let transformStyleAttribute: typeof TransformStyleAttribute;
+
+beforeAll(async () => {
+ if (import.meta.env.TEST_WASM === 'node') {
+ ({bundle, bundleAsync, transform, transformStyleAttribute} = await import(
+ '../../wasm/wasm-node.mjs'
+ ));
+ } else if (import.meta.env.TEST_WASM === 'browser') {
+ let wasm = await import('../../wasm/index.mjs');
+ await wasm.default();
+ ({transform, transformStyleAttribute} = wasm);
+ bundle = function (options) {
+ return wasm.bundle({
+ ...options,
+ resolver: {
+ read: (filePath) => fs.readFileSync(filePath, 'utf8'),
+ },
+ });
+ };
+
+ bundleAsync = function (options) {
+ if (!options.resolver?.read) {
+ options.resolver = {
+ ...options.resolver,
+ read: (filePath) => fs.readFileSync(filePath, 'utf8'),
+ };
+ }
+
+ return wasm.bundleAsync(options);
+ };
+ } else {
+ ({bundle, bundleAsync, transform, transformStyleAttribute} = await import(
+ '../index.mjs'
+ ));
}
-} else {
- ({ bundle, bundleAsync, transform, transformStyleAttribute } = await import('../index.mjs'));
-}
+});
test('px to rem', () => {
// Similar to https://github.com/cuth/postcss-pxtorem
@@ -55,14 +72,16 @@ test('px to rem', () => {
if (length.unit === 'px') {
return {
unit: 'rem',
- value: length.value / 16
+ value: length.value / 16,
};
}
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{--custom:calc(var(--foo) + 2rem);width:2rem;height:calc(100vh - 4rem)}');
+ expect(res.code.toString()).toBe(
+ '.foo{--custom:calc(var(--foo) + 2rem);width:2rem;height:calc(100vh - 4rem)}',
+ );
});
test('custom units', () => {
@@ -89,34 +108,36 @@ test('custom units', () => {
type: 'token',
value: {
type: 'number',
- value: token.value
- }
+ value: token.value,
+ },
},
{
type: 'token',
value: {
type: 'delim',
- value: '*'
- }
+ value: '*',
+ },
},
{
type: 'var',
value: {
name: {
- ident: token.unit
- }
- }
- }
- ]
- }
- }
+ ident: token.unit,
+ },
+ },
+ },
+ ],
+ },
+ };
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{--step:.25rem;font-size:calc(3*var(--step))}');
+ expect(res.code.toString()).toBe(
+ '.foo{--step:.25rem;font-size:calc(3*var(--step))}',
+ );
});
test('design tokens', () => {
@@ -129,16 +150,16 @@ test('design tokens', () => {
r: 255,
g: 0,
b: 0,
- alpha: 1
- }
+ alpha: 1,
+ },
},
'size.spacing.small': {
type: 'length',
value: {
unit: 'px',
- value: 16
- }
- }
+ value: 16,
+ },
+ },
};
let res = transform({
@@ -153,35 +174,38 @@ test('design tokens', () => {
visitor: {
Function: {
'design-token'(fn) {
- if (fn.arguments.length === 1 && fn.arguments[0].type === 'token' && fn.arguments[0].value.type === 'string') {
+ if (
+ fn.arguments.length === 1 &&
+ fn.arguments[0].type === 'token' &&
+ fn.arguments[0].value.type === 'string'
+ ) {
return tokens[fn.arguments[0].value.value];
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{color:red;padding:16px}');
+ expect(res.code.toString()).toBe('.foo{color:red;padding:16px}');
});
test('env function', () => {
// https://www.npmjs.com/package/postcss-env-function
- /** @type {Record} */
- let tokens = {
+ let tokens: Record = {
'--branding-small': {
type: 'length',
value: {
unit: 'px',
- value: 600
- }
+ value: 600,
+ },
},
'--branding-padding': {
type: 'length',
value: {
unit: 'px',
- value: 20
- }
- }
+ value: 20,
+ },
+ },
};
let res = transform({
@@ -200,31 +224,30 @@ test('env function', () => {
if (env.name.type === 'custom') {
return tokens[env.name.ident];
}
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
+ expect(res.code.toString()).toBe('@media (width<=600px){body{padding:20px}}');
});
test('specific environment variables', () => {
// https://www.npmjs.com/package/postcss-env-function
- /** @type {Record} */
- let tokens = {
+ let tokens: Record = {
'--branding-small': {
type: 'length',
value: {
unit: 'px',
- value: 600
- }
+ value: 600,
+ },
},
'--branding-padding': {
type: 'length',
value: {
unit: 'px',
- value: 20
- }
- }
+ value: 20,
+ },
+ },
};
let res = transform({
@@ -241,12 +264,12 @@ test('specific environment variables', () => {
visitor: {
EnvironmentVariable: {
'--branding-small': () => tokens['--branding-small'],
- '--branding-padding': () => tokens['--branding-padding']
- }
- }
+ '--branding-padding': () => tokens['--branding-padding'],
+ },
+ },
});
- assert.equal(res.code.toString(), '@media (width<=600px){body{padding:20px}}');
+ expect(res.code.toString()).toBe('@media (width<=600px){body{padding:20px}}');
});
test('url', () => {
@@ -263,11 +286,13 @@ test('url', () => {
Url(url) {
url.url = 'https://mywebsite.com/' + url.url;
return url;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{background:url(https://mywebsite.com/foo.png)}');
+ expect(res.code.toString()).toBe(
+ '.foo{background:url(https://mywebsite.com/foo.png)}',
+ );
});
test('static vars', () => {
@@ -288,19 +313,19 @@ test('static vars', () => {
unknown(rule) {
declared.set(rule.name, rule.prelude);
return [];
- }
+ },
},
Token: {
'at-keyword'(token) {
if (declared.has(token.value)) {
return declared.get(token.value);
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.menu_link{background:#056ef0}');
+ expect(res.code.toString()).toBe('.menu_link{background:#056ef0}');
});
test('selector prefix', () => {
@@ -315,12 +340,16 @@ test('selector prefix', () => {
`),
visitor: {
Selector(selector) {
- return [{ type: 'class', name: 'prefix' }, { type: 'combinator', value: 'descendant' }, ...selector];
- }
- }
+ return [
+ {type: 'class', name: 'prefix'},
+ {type: 'combinator', value: 'descendant'},
+ ...selector,
+ ];
+ },
+ },
});
- assert.equal(res.code.toString(), '.prefix .a,.prefix .b{color:red}');
+ expect(res.code.toString()).toBe('.prefix .a,.prefix .b{color:red}');
});
test('apply', () => {
@@ -343,13 +372,17 @@ test('apply', () => {
Rule: {
style(rule) {
for (let selector of rule.value.selectors) {
- if (selector.length === 1 && selector[0].type === 'type' && selector[0].name.startsWith('--')) {
+ if (
+ selector.length === 1 &&
+ selector[0].type === 'type' &&
+ selector[0].name.startsWith('--')
+ ) {
defined.set(selector[0].name, rule.value.declarations);
- return { type: 'ignored', value: null };
+ return {type: 'ignored', value: null};
}
}
- rule.value.rules = rule.value.rules.filter(child => {
+ rule.value.rules = rule.value.rules.filter((child) => {
if (child.type === 'unknown' && child.value.name === 'apply') {
for (let token of child.value.prelude) {
if (token.type === 'dashed-ident' && defined.has(token.value)) {
@@ -365,12 +398,14 @@ test('apply', () => {
});
return rule;
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.toolbar{color:#fff;border:1px solid green}');
+ expect(res.code.toString()).toBe(
+ '.toolbar{color:#fff;border:1px solid green}',
+ );
});
test('property lookup', () => {
@@ -389,37 +424,43 @@ test('property lookup', () => {
style(rule) {
let valuesByProperty = new Map();
for (let decl of rule.value.declarations.declarations) {
- /** @type string */
let name = decl.property;
if (decl.property === 'unparsed') {
- name = decl.value.propertyId.property;
+ name = decl.value.propertyId.property as typeof name;
}
valuesByProperty.set(name, decl);
}
- rule.value.declarations.declarations = rule.value.declarations.declarations.map(decl => {
- // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
- if (decl.property === 'unparsed' && decl.value.value.length === 1) {
- let token = decl.value.value[0];
- if (token.type === 'token' && token.value.type === 'at-keyword' && valuesByProperty.has(token.value.value)) {
- let v = valuesByProperty.get(token.value.value);
- return {
- /** @type any */
- property: decl.value.propertyId.property,
- value: v.value
- };
+ rule.value.declarations.declarations =
+ rule.value.declarations.declarations.map((decl) => {
+ // Only single value supported. Would need a way to convert parsed values to unparsed tokens otherwise.
+ if (
+ decl.property === 'unparsed' &&
+ decl.value.value.length === 1
+ ) {
+ let token = decl.value.value[0];
+ if (
+ token.type === 'token' &&
+ token.value.type === 'at-keyword' &&
+ valuesByProperty.has(token.value.value)
+ ) {
+ let v = valuesByProperty.get(token.value.value);
+ return {
+ property: decl.value.propertyId.property,
+ value: v.value,
+ };
+ }
}
- }
- return decl;
- });
+ return decl;
+ }) as typeof rule.value.declarations.declarations;
return rule;
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.test{margin-left:20px;margin-right:20px}');
+ expect(res.code.toString()).toBe('.test{margin-left:20px;margin-right:20px}');
});
test('focus visible', () => {
@@ -433,33 +474,41 @@ test('focus visible', () => {
}
`),
targets: {
- safari: 14 << 16
+ safari: 14 << 16,
},
visitor: {
Rule: {
style(rule) {
- let clone = null;
+ let clone: SelectorList | null = null;
for (let selector of rule.value.selectors) {
for (let [i, component] of selector.entries()) {
- if (component.type === 'pseudo-class' && component.kind === 'focus-visible') {
+ if (
+ component.type === 'pseudo-class' &&
+ component.kind === 'focus-visible'
+ ) {
if (clone == null) {
- clone = [...rule.value.selectors.map(s => [...s])];
+ clone = [...rule.value.selectors.map((s) => [...s])];
}
- selector[i] = { type: 'class', name: 'focus-visible' };
+ selector[i] = {type: 'class', name: 'focus-visible'};
}
}
}
if (clone) {
- return [rule, { type: 'style', value: { ...rule.value, selectors: clone } }];
+ return [
+ rule,
+ {type: 'style', value: {...rule.value, selectors: clone}},
+ ];
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.test.focus-visible{color:red}.test:focus-visible{color:red}');
+ expect(res.code.toString()).toBe(
+ '.test.focus-visible{color:red}.test:focus-visible{color:red}',
+ );
});
test('dark theme class', () => {
@@ -478,45 +527,63 @@ test('dark theme class', () => {
Rule: {
media(rule) {
let q = rule.value.query.mediaQueries[0];
- if (q.condition?.type === 'feature' && q.condition.value.type === 'plain' && q.condition.value.name === 'prefers-color-scheme' && q.condition.value.value.value === 'dark') {
- /** @type {import('../ast').Rule[]} */
- let clonedRules = [rule];
+ if (
+ q.condition?.type === 'feature' &&
+ q.condition.value.type === 'plain' &&
+ q.condition.value.name === 'prefers-color-scheme' &&
+ q.condition.value.value.value === 'dark'
+ ) {
+ let clonedRules: Rule[] = [rule];
for (let r of rule.value.rules) {
if (r.type === 'style') {
- /** @type {import('../ast').Selector[]} */
- let clonedSelectors = [];
+ let clonedSelectors: Selector[] = [];
for (let selector of r.value.selectors) {
clonedSelectors.push([
- { type: 'type', name: 'html' },
- { type: 'attribute', name: 'theme', operation: { operator: 'equal', value: 'dark' } },
- { type: 'combinator', value: 'descendant' },
- ...selector
+ {type: 'type', name: 'html'},
+ {
+ type: 'attribute',
+ name: 'theme',
+ operation: {operator: 'equal', value: 'dark'},
+ },
+ {type: 'combinator', value: 'descendant'},
+ ...selector,
]);
selector.unshift(
- { type: 'type', name: 'html' },
+ {type: 'type', name: 'html'},
{
type: 'pseudo-class',
kind: 'not',
selectors: [
- [{ type: 'attribute', name: 'theme', operation: { operator: 'equal', value: 'light' } }]
- ]
+ [
+ {
+ type: 'attribute',
+ name: 'theme',
+ operation: {operator: 'equal', value: 'light'},
+ },
+ ],
+ ],
},
- { type: 'combinator', value: 'descendant' }
+ {type: 'combinator', value: 'descendant'},
);
}
- clonedRules.push({ type: 'style', value: { ...r.value, selectors: clonedSelectors } });
+ clonedRules.push({
+ type: 'style',
+ value: {...r.value, selectors: clonedSelectors},
+ });
}
}
return clonedRules;
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '@media (prefers-color-scheme:dark){html:not([theme=light]) body{background:#000}}html[theme=dark] body{background:#000}');
+ expect(res.code.toString()).toBe(
+ '@media (prefers-color-scheme:dark){html:not([theme=light]) body{background:#000}}html[theme=dark] body{background:#000}',
+ );
});
test('100vh fix', () => {
@@ -535,7 +602,13 @@ test('100vh fix', () => {
style(style) {
let cloned;
for (let property of style.value.declarations.declarations) {
- if (property.property === 'height' && property.value.type === 'length-percentage' && property.value.value.type === 'dimension' && property.value.value.value.unit === 'vh' && property.value.value.value.value === 100) {
+ if (
+ property.property === 'height' &&
+ property.value.type === 'length-percentage' &&
+ property.value.value.type === 'dimension' &&
+ property.value.value.value.unit === 'vh' &&
+ property.value.value.value.value === 100
+ ) {
if (!cloned) {
cloned = structuredClone(style);
cloned.value.declarations.declarations = [];
@@ -544,34 +617,39 @@ test('100vh fix', () => {
...property,
value: {
type: 'stretch',
- vendorPrefix: ['webkit']
- }
+ vendorPrefix: ['webkit'],
+ },
});
}
}
if (cloned) {
- return [style, {
- type: 'supports',
- value: {
- condition: {
- type: 'declaration',
- propertyId: {
- property: '-webkit-touch-callout'
+ return [
+ style,
+ {
+ type: 'supports',
+ value: {
+ condition: {
+ type: 'declaration',
+ propertyId: {
+ property: '-webkit-touch-callout',
+ },
+ value: 'none',
},
- value: 'none'
+ loc: style.value.loc,
+ rules: [cloned],
},
- loc: style.value.loc,
- rules: [cloned]
- }
- }];
+ },
+ ];
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{color:red;height:100vh}@supports (-webkit-touch-callout:none){.foo{height:-webkit-fill-available}}')
+ expect(res.code.toString()).toBe(
+ '.foo{color:red;height:100vh}@supports (-webkit-touch-callout:none){.foo{height:-webkit-fill-available}}',
+ );
});
test('logical transforms', () => {
@@ -595,11 +673,10 @@ test('logical transforms', () => {
visitor: {
Rule: {
style(style) {
- /** @type any */
- let cloned;
+ let cloned: any;
for (let property of style.value.declarations.declarations) {
if (property.property === 'transform') {
- let clonedTransforms = property.value.map(transform => {
+ let clonedTransforms = property.value.map((transform) => {
if (transform.type !== 'translateX') {
return transform;
}
@@ -612,27 +689,41 @@ test('logical transforms', () => {
let value;
switch (transform.value.type) {
case 'dimension':
- value = { type: 'dimension', value: { unit: transform.value.value.unit, value: -transform.value.value.value } };
+ value = {
+ type: 'dimension',
+ value: {
+ unit: transform.value.value.unit,
+ value: -transform.value.value.value,
+ },
+ };
break;
case 'percentage':
- value = { type: 'percentage', value: -transform.value.value };
+ value = {type: 'percentage', value: -transform.value.value};
break;
case 'calc':
- value = { type: 'calc', value: { type: 'product', value: [-1, transform.value.value] } };
+ value = {
+ type: 'calc',
+ value: {
+ type: 'product',
+ value: [-1, transform.value.value],
+ },
+ };
break;
}
return {
type: 'translateX',
- value
- }
+ value,
+ };
});
if (cloned) {
- cloned.value.selectors.at(-1).push({ type: 'pseudo-class', kind: 'dir', direction: 'rtl' });
+ cloned.value.selectors
+ .at(-1)
+ .push({type: 'pseudo-class', kind: 'dir', direction: 'rtl'});
cloned.value.declarations.declarations.push({
...property,
- value: clonedTransforms
+ value: clonedTransforms,
});
}
}
@@ -641,12 +732,14 @@ test('logical transforms', () => {
if (cloned) {
return [style, cloned];
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{transform:translate(50px)}.foo:dir(rtl){transform:translate(-50px)}.bar{transform:translate(20%)}.bar:dir(rtl){transform:translate(-20%)}.baz{transform:translate(calc(100vw - 20px))}.baz:dir(rtl){transform:translate(-1*calc(100vw - 20px))}');
+ expect(res.code.toString()).toBe(
+ '.foo{transform:translate(50px)}.foo:dir(rtl){transform:translate(-50px)}.bar{transform:translate(20%)}.bar:dir(rtl){transform:translate(-20%)}.baz{transform:translate(calc(100vw - 20px))}.baz:dir(rtl){transform:translate(-1*calc(100vw - 20px))}',
+ );
});
test('hover media query', () => {
@@ -675,26 +768,32 @@ test('hover media query', () => {
for (let rule of media.value.rules) {
if (rule.type === 'style') {
for (let selector of rule.value.selectors) {
- selector.unshift({ type: 'class', name: 'hoverable' }, { type: 'combinator', value: 'descendant' });
+ selector.unshift(
+ {type: 'class', name: 'hoverable'},
+ {type: 'combinator', value: 'descendant'},
+ );
}
}
}
- return media.value.rules
+ return media.value.rules;
}
- }
- }
- }
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.hoverable .foo{color:red}');
+ expect(res.code.toString()).toBe('.hoverable .foo{color:red}');
});
test('momentum scrolling', () => {
// Similar to https://github.com/yunusga/postcss-momentum-scrolling
- let visitOverflow = decl => [decl, {
- property: '-webkit-overflow-scrolling',
- raw: 'touch'
- }];
+ let visitOverflow = (decl) => [
+ decl,
+ {
+ property: '-webkit-overflow-scrolling',
+ raw: 'touch',
+ },
+ ];
let res = transform({
filename: 'test.css',
@@ -708,12 +807,14 @@ test('momentum scrolling', () => {
Declaration: {
overflow: visitOverflow,
'overflow-x': visitOverflow,
- 'overflow-y': visitOverflow
- }
- }
+ 'overflow-y': visitOverflow,
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{-webkit-overflow-scrolling:touch;overflow:auto}');
+ expect(res.code.toString()).toBe(
+ '.foo{-webkit-overflow-scrolling:touch;overflow:auto}',
+ );
});
test('size', () => {
@@ -731,20 +832,22 @@ test('size', () => {
custom: {
size(property) {
if (property.value[0].type === 'length') {
- /** @type {import('../ast').Size} */
- let value = { type: 'length-percentage', value: { type: 'dimension', value: property.value[0].value } };
+ let value: Size = {
+ type: 'length-percentage',
+ value: {type: 'dimension', value: property.value[0].value},
+ };
return [
- { property: 'width', value },
- { property: 'height', value }
+ {property: 'width', value},
+ {property: 'height', value},
];
}
- }
- }
- }
- }
+ },
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{width:12px;height:12px}');
+ expect(res.code.toString()).toBe('.foo{width:12px;height:12px}');
});
test('works with style attributes', () => {
@@ -757,14 +860,14 @@ test('works with style attributes', () => {
if (length.unit === 'px') {
return {
unit: 'rem',
- value: length.value / 16
+ value: length.value / 16,
};
}
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), 'height:calc(100vh - 4rem)');
+ expect(res.code.toString()).toBe('height:calc(100vh - 4rem)');
});
test('works with bundler', () => {
@@ -776,14 +879,16 @@ test('works with bundler', () => {
if (length.unit === 'px') {
return {
unit: 'rem',
- value: length.value / 16
+ value: length.value / 16,
};
}
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.b{height:calc(100vh - 4rem)}.a{width:2rem}');
+ expect(res.code.toString()).toBe(
+ '.b{height:calc(100vh - 4rem)}.a{width:2rem}',
+ );
});
test('works with async bundler', async () => {
@@ -795,14 +900,16 @@ test('works with async bundler', async () => {
if (length.unit === 'px') {
return {
unit: 'rem',
- value: length.value / 16
+ value: length.value / 16,
};
}
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.b{height:calc(100vh - 4rem)}.a{width:2rem}');
+ expect(res.code.toString()).toBe(
+ '.b{height:calc(100vh - 4rem)}.a{width:2rem}',
+ );
});
test('dashed idents', () => {
@@ -818,11 +925,13 @@ test('dashed idents', () => {
visitor: {
DashedIdent(ident) {
return `--prefix-${ident.slice(2)}`;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{--prefix-foo:#ff0;color:var(--prefix-foo)}');
+ expect(res.code.toString()).toBe(
+ '.foo{--prefix-foo:#ff0;color:var(--prefix-foo)}',
+ );
});
test('custom idents', () => {
@@ -841,11 +950,13 @@ test('custom idents', () => {
visitor: {
CustomIdent(ident) {
return `prefix-${ident}`;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '@keyframes prefix-test{0%{color:red}to{color:green}}.foo{animation:prefix-test}');
+ expect(res.code.toString()).toBe(
+ '@keyframes prefix-test{0%{color:red}to{color:green}}.foo{animation:prefix-test}',
+ );
});
test('returning string values', () => {
@@ -862,46 +973,46 @@ test('returning string values', () => {
type: 'style',
value: {
loc: rule.loc,
- selectors: [
- [{ type: 'universal' }]
- ],
+ selectors: [[{type: 'universal'}]],
declarations: {
declarations: [
{
property: 'visibility',
- raw: 'hi\\64 den' // escapes work for raw but not value
+ raw: 'hi\\64 den', // escapes work for raw but not value
},
{
property: 'background',
- raw: 'yellow'
+ raw: 'yellow',
},
{
property: '--custom',
- raw: 'hi'
+ raw: 'hi',
},
{
property: 'transition',
vendorPrefix: ['moz'],
- raw: '200ms test'
+ raw: '200ms test',
},
{
property: '-webkit-animation',
- raw: '3s cubic-bezier(0.25, 0.1, 0.25, 1) foo'
- }
- ]
- }
- }
- }
- }
- }
- }
+ raw: '3s cubic-bezier(0.25, 0.1, 0.25, 1) foo',
+ },
+ ],
+ },
+ },
+ };
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '*{visibility:hidden;--custom:hi;background:#ff0;-moz-transition:test .2s;-webkit-animation:3s foo}');
+ expect(res.code.toString()).toBe(
+ '*{visibility:hidden;--custom:hi;background:#ff0;-moz-transition:test .2s;-webkit-animation:3s foo}',
+ );
});
test('errors on invalid dashed idents', () => {
- assert.throws(() => {
+ expect(() =>
transform({
filename: 'test.css',
minify: true,
@@ -912,25 +1023,28 @@ test('errors on invalid dashed idents', () => {
`),
visitor: {
Function(fn) {
- if (fn.arguments[0].type === 'token' && fn.arguments[0].value.type === 'ident') {
+ if (
+ fn.arguments[0].type === 'token' &&
+ fn.arguments[0].value.type === 'ident'
+ ) {
fn.arguments = [
{
type: 'var',
value: {
- name: { ident: fn.arguments[0].value.value }
- }
- }
+ name: {ident: fn.arguments[0].value.value},
+ },
+ },
];
}
return {
type: 'function',
- value: fn
- }
- }
- }
- })
- }, 'Dashed idents must start with --');
+ value: fn,
+ };
+ },
+ },
+ }),
+ ).toThrowError('Dashed idents must start with --');
});
test('supports returning raw values for tokens', () => {
@@ -945,13 +1059,13 @@ test('supports returning raw values for tokens', () => {
visitor: {
Function: {
theme() {
- return { raw: 'rgba(255, 0, 0)' };
- }
- }
- }
+ return {raw: 'rgba(255, 0, 0)'};
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{color:red}');
+ expect(res.code.toString()).toBe('.foo{color:red}');
});
test('supports returning raw values as variables', () => {
@@ -959,7 +1073,7 @@ test('supports returning raw values as variables', () => {
filename: 'test.css',
minify: true,
cssModules: {
- dashedIdents: true
+ dashedIdents: true,
},
code: Buffer.from(`
.foo {
@@ -969,13 +1083,13 @@ test('supports returning raw values as variables', () => {
visitor: {
Function: {
theme() {
- return { raw: 'var(--foo)' };
- }
- }
- }
+ return {raw: 'var(--foo)'};
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.EgL3uq_foo{color:var(--EgL3uq_foo)}');
+ expect(res.code.toString()).toBe('.EgL3uq_foo{color:var(--EgL3uq_foo)}');
});
test('works with currentColor', () => {
@@ -990,11 +1104,11 @@ test('works with currentColor', () => {
visitor: {
Rule(rule) {
return rule;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.foo{color:currentColor}');
+ expect(res.code.toString()).toBe('.foo{color:currentColor}');
});
test('nth of S to nth-of-type', () => {
@@ -1009,17 +1123,22 @@ test('nth of S to nth-of-type', () => {
visitor: {
Selector(selector) {
for (let component of selector) {
- if (component.type === 'pseudo-class' && component.kind === 'nth-child' && component.of) {
+ if (
+ component.type === 'pseudo-class' &&
+ component.kind === 'nth-child' &&
+ component.of
+ ) {
delete component.of;
+ // @ts-ignore
component.kind = 'nth-of-type';
}
}
return selector;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), 'a:nth-of-type(2n){color:red}');
+ expect(res.code.toString()).toBe('a:nth-of-type(2n){color:red}');
});
test('media query raw', () => {
@@ -1036,15 +1155,14 @@ test('media query raw', () => {
customAtRules: {
breakpoints: {
prelude: null,
- body: "rule-list",
+ body: 'rule-list',
},
},
visitor: {
Rule: {
custom: {
- breakpoints({ body, loc }) {
- /** @type {import('lightningcss').ReturnedRule[]} */
- const value = [];
+ breakpoints({body, loc}) {
+ const value: ReturnedRule[] = [];
for (let rule of body.value) {
if (rule.type !== 'style') {
@@ -1061,27 +1179,27 @@ test('media query raw', () => {
value.push(rule);
value.push({
- type: "media",
+ type: 'media',
value: {
rules: [clone],
loc,
query: {
- mediaQueries: [
- { raw: '(min-width: 500px)' }
- ]
- }
- }
+ mediaQueries: [{raw: '(min-width: 500px)'}],
+ },
+ },
});
}
return value;
- }
- }
- }
- }
+ },
+ },
+ },
+ },
});
- assert.equal(res.code.toString(), '.m-1{margin:10px}@media (width>=500px){.sm\\:m-1{margin:10px}}');
+ expect(res.code.toString()).toBe(
+ '.m-1{margin:10px}@media (width>=500px){.sm\\:m-1{margin:10px}}',
+ );
});
test('visit stylesheet', () => {
@@ -1099,13 +1217,17 @@ test('visit stylesheet', () => {
`),
visitor: {
StyleSheetExit(stylesheet) {
- stylesheet.rules.sort((a, b) => a.value.selectors[0][0].name.localeCompare(b.value.selectors[0][0].name));
+ stylesheet.rules.sort((a, b) =>
+ // @ts-expect-error TODO: fix type error
+ a.value.selectors[0][0].name.localeCompare(
+ // @ts-expect-error TODO: fix type error
+ b.value.selectors[0][0].name,
+ ),
+ );
return stylesheet;
- }
- }
+ },
+ },
});
- assert.equal(res.code.toString(), '.bar{width:80px}.foo{width:32px}');
+ expect(res.code.toString()).toBe('.bar{width:80px}.foo{width:32px}');
});
-
-test.run();
diff --git a/node/tsconfig.json b/node/tsconfig.json
index 9b82eaa4..3e2f817c 100644
--- a/node/tsconfig.json
+++ b/node/tsconfig.json
@@ -5,6 +5,7 @@
"moduleResolution": "node",
"isolatedModules": true,
"noEmit": true,
- "strict": true
+ "strict": true,
+ "skipLibCheck": true
}
}
diff --git a/package.json b/package.json
index b4c1f4bf..abcdca53 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"sharp": "^0.33.5",
"typescript": "^5.7.2",
"util": "^0.12.4",
- "uvu": "^0.5.6"
+ "vitest": "^3.1.2"
},
"resolutions": {
"lightningcss": "link:."
@@ -92,6 +92,6 @@
"website:build": "yarn wasm:build-release && parcel build 'website/*.html' website/playground/index.html",
"build-ast": "cargo run --example schema --features jsonschema && node scripts/build-ast.js",
"tsc": "tsc -p node/tsconfig.json",
- "test": "uvu node/test"
+ "test": "vitest run node/test"
}
}
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
new file mode 100644
index 00000000..c6f8e45b
--- /dev/null
+++ b/vitest.workspace.ts
@@ -0,0 +1,27 @@
+import {defineWorkspace} from 'vitest/config';
+
+export default defineWorkspace([
+ {
+ test: {
+ name: 'Node.js',
+ environment: 'node',
+ restoreMocks: true,
+ },
+ },
+
+ // TODO: enable for browser tests
+ // {
+ // test: {
+ // name: 'Browser',
+ // restoreMocks: true,
+ // env: {
+ // TEST_WASM: 'browser',
+ // },
+ // browser: {
+ // provider: 'playwright',
+ // enabled: true,
+ // instances: [{browser: 'chromium'}],
+ // },
+ // },
+ // },
+]);
diff --git a/yarn.lock b/yarn.lock
index 2c0ecdf5..cf96d847 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -238,116 +238,241 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f"
integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==
+"@esbuild/aix-ppc64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz#b87036f644f572efb2b3c75746c97d1d2d87ace8"
+ integrity sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==
+
"@esbuild/android-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4"
integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==
+"@esbuild/android-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz#5ca7dc20a18f18960ad8d5e6ef5cf7b0a256e196"
+ integrity sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==
+
"@esbuild/android-arm@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824"
integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==
+"@esbuild/android-arm@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.2.tgz#3c49f607b7082cde70c6ce0c011c362c57a194ee"
+ integrity sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==
+
"@esbuild/android-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d"
integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==
+"@esbuild/android-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.2.tgz#8a00147780016aff59e04f1036e7cb1b683859e2"
+ integrity sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==
+
"@esbuild/darwin-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e"
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
+"@esbuild/darwin-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz#486efe7599a8d90a27780f2bb0318d9a85c6c423"
+ integrity sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==
+
"@esbuild/darwin-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd"
integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==
+"@esbuild/darwin-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz#95ee222aacf668c7a4f3d7ee87b3240a51baf374"
+ integrity sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==
+
"@esbuild/freebsd-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487"
integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==
+"@esbuild/freebsd-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz#67efceda8554b6fc6a43476feba068fb37fa2ef6"
+ integrity sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==
+
"@esbuild/freebsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c"
integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==
+"@esbuild/freebsd-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz#88a9d7ecdd3adadbfe5227c2122d24816959b809"
+ integrity sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==
+
"@esbuild/linux-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b"
integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==
+"@esbuild/linux-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz#87be1099b2bbe61282333b084737d46bc8308058"
+ integrity sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==
+
"@esbuild/linux-arm@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef"
integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==
+"@esbuild/linux-arm@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz#72a285b0fe64496e191fcad222185d7bf9f816f6"
+ integrity sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==
+
"@esbuild/linux-ia32@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601"
integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==
+"@esbuild/linux-ia32@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz#337a87a4c4dd48a832baed5cbb022be20809d737"
+ integrity sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==
+
"@esbuild/linux-loong64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299"
integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==
+"@esbuild/linux-loong64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz#1b81aa77103d6b8a8cfa7c094ed3d25c7579ba2a"
+ integrity sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==
+
"@esbuild/linux-mips64el@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec"
integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==
+"@esbuild/linux-mips64el@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz#afbe380b6992e7459bf7c2c3b9556633b2e47f30"
+ integrity sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==
+
"@esbuild/linux-ppc64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8"
integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==
+"@esbuild/linux-ppc64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz#6bf8695cab8a2b135cca1aa555226dc932d52067"
+ integrity sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==
+
"@esbuild/linux-riscv64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf"
integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==
+"@esbuild/linux-riscv64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz#43c2d67a1a39199fb06ba978aebb44992d7becc3"
+ integrity sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==
+
"@esbuild/linux-s390x@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8"
integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==
+"@esbuild/linux-s390x@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz#419e25737ec815c6dce2cd20d026e347cbb7a602"
+ integrity sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==
+
"@esbuild/linux-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78"
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
+"@esbuild/linux-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb"
+ integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==
+
+"@esbuild/netbsd-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz#744affd3b8d8236b08c5210d828b0698a62c58ac"
+ integrity sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==
+
"@esbuild/netbsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b"
integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==
+"@esbuild/netbsd-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz#dbbe7521fd6d7352f34328d676af923fc0f8a78f"
+ integrity sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==
+
+"@esbuild/openbsd-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz#f9caf987e3e0570500832b487ce3039ca648ce9f"
+ integrity sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==
+
"@esbuild/openbsd-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0"
integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==
+"@esbuild/openbsd-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz#d2bb6a0f8ffea7b394bb43dfccbb07cabd89f768"
+ integrity sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==
+
"@esbuild/sunos-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30"
integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==
+"@esbuild/sunos-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz#49b437ed63fe333b92137b7a0c65a65852031afb"
+ integrity sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==
+
"@esbuild/win32-arm64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae"
integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==
+"@esbuild/win32-arm64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz#081424168463c7d6c7fb78f631aede0c104373cf"
+ integrity sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==
+
"@esbuild/win32-ia32@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67"
integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==
+"@esbuild/win32-ia32@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz#3f9e87143ddd003133d21384944a6c6cadf9693f"
+ integrity sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==
+
"@esbuild/win32-x64@0.19.12":
version "0.19.12"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
+"@esbuild/win32-x64@0.25.2":
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d"
+ integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==
+
"@img/sharp-darwin-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
@@ -480,7 +605,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
-"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
@@ -1301,6 +1426,106 @@
"@parcel/utils" "2.13.3"
nullthrows "^1.1.1"
+"@rollup/rollup-android-arm-eabi@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3"
+ integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==
+
+"@rollup/rollup-android-arm64@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3"
+ integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==
+
+"@rollup/rollup-darwin-arm64@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz#ef439182c739b20b3c4398cfc03e3c1249ac8903"
+ integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==
+
+"@rollup/rollup-darwin-x64@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504"
+ integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==
+
+"@rollup/rollup-freebsd-arm64@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936"
+ integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==
+
+"@rollup/rollup-freebsd-x64@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b"
+ integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c"
+ integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==
+
+"@rollup/rollup-linux-arm-musleabihf@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7"
+ integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==
+
+"@rollup/rollup-linux-arm64-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c"
+ integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==
+
+"@rollup/rollup-linux-arm64-musl@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552"
+ integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==
+
+"@rollup/rollup-linux-loongarch64-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6"
+ integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83"
+ integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==
+
+"@rollup/rollup-linux-riscv64-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3"
+ integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==
+
+"@rollup/rollup-linux-riscv64-musl@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039"
+ integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==
+
+"@rollup/rollup-linux-s390x-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b"
+ integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==
+
+"@rollup/rollup-linux-x64-gnu@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0"
+ integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==
+
+"@rollup/rollup-linux-x64-musl@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282"
+ integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==
+
+"@rollup/rollup-win32-arm64-msvc@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b"
+ integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==
+
+"@rollup/rollup-win32-ia32-msvc@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921"
+ integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==
+
+"@rollup/rollup-win32-x64-msvc@4.40.0":
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76"
+ integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==
+
"@swc/core-darwin-arm64@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz#faaaab19b4a039ae67ef661c0144a6f20fe8a78e"
@@ -1394,6 +1619,11 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+"@types/estree@1.0.7", "@types/estree@^1.0.0":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
+ integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
+
"@types/glob@^7.1.3":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -1436,6 +1666,65 @@
dependencies:
"@types/node" "*"
+"@vitest/expect@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.1.2.tgz#b203a7ad2efa6af96c85f6c116216bda259d2bc8"
+ integrity sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==
+ dependencies:
+ "@vitest/spy" "3.1.2"
+ "@vitest/utils" "3.1.2"
+ chai "^5.2.0"
+ tinyrainbow "^2.0.0"
+
+"@vitest/mocker@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.1.2.tgz#1ff239036072feb543ab56825ada09b12a075af2"
+ integrity sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==
+ dependencies:
+ "@vitest/spy" "3.1.2"
+ estree-walker "^3.0.3"
+ magic-string "^0.30.17"
+
+"@vitest/pretty-format@3.1.2", "@vitest/pretty-format@^3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.1.2.tgz#689b0604c0b73fdccb144f11b64d70c9233b23b8"
+ integrity sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==
+ dependencies:
+ tinyrainbow "^2.0.0"
+
+"@vitest/runner@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.1.2.tgz#ffeba74618046221e944e94f09b565af772170cf"
+ integrity sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==
+ dependencies:
+ "@vitest/utils" "3.1.2"
+ pathe "^2.0.3"
+
+"@vitest/snapshot@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.1.2.tgz#46c52a417afbf1fe94fba0a5735cbedf9cfc60f6"
+ integrity sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==
+ dependencies:
+ "@vitest/pretty-format" "3.1.2"
+ magic-string "^0.30.17"
+ pathe "^2.0.3"
+
+"@vitest/spy@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.1.2.tgz#3a5be04d71c4a458c8d6859503626e2aed61bcbf"
+ integrity sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==
+ dependencies:
+ tinyspy "^3.0.2"
+
+"@vitest/utils@3.1.2":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.1.2.tgz#f3ae55b3a205c88c346a2a8dcde7c89210364932"
+ integrity sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==
+ dependencies:
+ "@vitest/pretty-format" "3.1.2"
+ loupe "^3.1.3"
+ tinyrainbow "^2.0.0"
+
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
@@ -1493,6 +1782,11 @@ assert@^2.0.0:
object.assign "^4.1.4"
util "^0.12.5"
+assertion-error@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
+ integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
+
ast-types@0.15.2:
version "0.15.2"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d"
@@ -1603,6 +1897,11 @@ buffer@^5.2.1, buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
+cac@^6.7.14:
+ version "6.7.14"
+ resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
+ integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
+
call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840"
@@ -1659,6 +1958,17 @@ caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001704:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990"
integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==
+chai@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05"
+ integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==
+ dependencies:
+ assertion-error "^2.0.1"
+ check-error "^2.1.1"
+ deep-eql "^5.0.1"
+ loupe "^3.1.0"
+ pathval "^2.0.0"
+
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -1676,6 +1986,11 @@ chalk@^4.0.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+check-error@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
+ integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
+
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -1922,7 +2237,7 @@ data-uri-to-buffer@^4.0.0:
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
-debug@4, debug@^4.1.0, debug@^4.1.1:
+debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -1936,6 +2251,11 @@ debug@4.3.2:
dependencies:
ms "2.1.2"
+deep-eql@^5.0.1:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
+ integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
+
define-data-property@^1.0.1, define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
@@ -1954,11 +2274,6 @@ define-properties@^1.1.3, define-properties@^1.2.1:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
-dequal@^2.0.0:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
- integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
-
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@@ -1979,11 +2294,6 @@ diff-sequences@^27.5.1:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
-diff@^5.0.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531"
- integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
-
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -2121,6 +2431,11 @@ es-errors@^1.3.0:
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+es-module-lexer@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21"
+ integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==
+
es-object-atoms@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941"
@@ -2194,6 +2509,37 @@ esbuild@^0.19.8:
"@esbuild/win32-ia32" "0.19.12"
"@esbuild/win32-x64" "0.19.12"
+esbuild@^0.25.0:
+ version "0.25.2"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.2.tgz#55a1d9ebcb3aa2f95e8bba9e900c1a5061bc168b"
+ integrity sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.25.2"
+ "@esbuild/android-arm" "0.25.2"
+ "@esbuild/android-arm64" "0.25.2"
+ "@esbuild/android-x64" "0.25.2"
+ "@esbuild/darwin-arm64" "0.25.2"
+ "@esbuild/darwin-x64" "0.25.2"
+ "@esbuild/freebsd-arm64" "0.25.2"
+ "@esbuild/freebsd-x64" "0.25.2"
+ "@esbuild/linux-arm" "0.25.2"
+ "@esbuild/linux-arm64" "0.25.2"
+ "@esbuild/linux-ia32" "0.25.2"
+ "@esbuild/linux-loong64" "0.25.2"
+ "@esbuild/linux-mips64el" "0.25.2"
+ "@esbuild/linux-ppc64" "0.25.2"
+ "@esbuild/linux-riscv64" "0.25.2"
+ "@esbuild/linux-s390x" "0.25.2"
+ "@esbuild/linux-x64" "0.25.2"
+ "@esbuild/netbsd-arm64" "0.25.2"
+ "@esbuild/netbsd-x64" "0.25.2"
+ "@esbuild/openbsd-arm64" "0.25.2"
+ "@esbuild/openbsd-x64" "0.25.2"
+ "@esbuild/sunos-x64" "0.25.2"
+ "@esbuild/win32-arm64" "0.25.2"
+ "@esbuild/win32-ia32" "0.25.2"
+ "@esbuild/win32-x64" "0.25.2"
+
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
@@ -2219,6 +2565,13 @@ esprima@~4.0.0:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+estree-walker@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
+ integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
+ dependencies:
+ "@types/estree" "^1.0.0"
+
event-emitter@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
@@ -2227,6 +2580,11 @@ event-emitter@^0.3.5:
d "1"
es5-ext "~0.10.14"
+expect-type@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f"
+ integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==
+
ext@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
@@ -2257,6 +2615,11 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
+fdir@^6.4.3, fdir@^6.4.4:
+ version "6.4.4"
+ resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9"
+ integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
+
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
@@ -2340,6 +2703,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
@@ -2724,11 +3092,6 @@ klaw-sync@^6.0.0:
dependencies:
graceful-fs "^4.1.11"
-kleur@^4.0.3:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
- integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
-
lightningcss@^1.22.1:
version "0.0.0"
uid ""
@@ -2794,6 +3157,11 @@ lodash@^4.17.20, lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+loupe@^3.1.0, loupe@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2"
+ integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==
+
lru-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
@@ -2801,6 +3169,13 @@ lru-queue@^0.1.0:
dependencies:
es5-ext "~0.10.2"
+magic-string@^0.30.17:
+ version "0.30.17"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
+ integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+
markdown-it-anchor@^8.6.6:
version "8.6.7"
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634"
@@ -2898,11 +3273,6 @@ mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-mri@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
- integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
-
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -2948,6 +3318,11 @@ nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
+nanoid@^3.3.8:
+ version "3.3.11"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
napi-wasm@^1.0.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.3.tgz#7bb95c88e6561f84880bb67195437b1cfbe99224"
@@ -3187,6 +3562,16 @@ path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+pathe@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
+ integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
+
+pathval@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25"
+ integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==
+
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -3202,6 +3587,11 @@ picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+picomatch@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
+ integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
+
pkg-dir@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -3435,6 +3825,15 @@ postcss@^8.3.11:
picocolors "^1.1.1"
source-map-js "^1.2.1"
+postcss@^8.5.3:
+ version "8.5.3"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
+ integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
+ dependencies:
+ nanoid "^3.3.8"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
posthtml-expressions@^1.7.1:
version "1.11.4"
resolved "https://registry.yarnpkg.com/posthtml-expressions/-/posthtml-expressions-1.11.4.tgz#eb86666de10940268a74fe0f3fb62d3f7607b5de"
@@ -3676,12 +4075,34 @@ rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
-sade@^1.7.3:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
- integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
+rollup@^4.34.9:
+ version "4.40.0"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.0.tgz#13742a615f423ccba457554f006873d5a4de1920"
+ integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==
dependencies:
- mri "^1.1.0"
+ "@types/estree" "1.0.7"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.40.0"
+ "@rollup/rollup-android-arm64" "4.40.0"
+ "@rollup/rollup-darwin-arm64" "4.40.0"
+ "@rollup/rollup-darwin-x64" "4.40.0"
+ "@rollup/rollup-freebsd-arm64" "4.40.0"
+ "@rollup/rollup-freebsd-x64" "4.40.0"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.40.0"
+ "@rollup/rollup-linux-arm-musleabihf" "4.40.0"
+ "@rollup/rollup-linux-arm64-gnu" "4.40.0"
+ "@rollup/rollup-linux-arm64-musl" "4.40.0"
+ "@rollup/rollup-linux-loongarch64-gnu" "4.40.0"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.40.0"
+ "@rollup/rollup-linux-riscv64-gnu" "4.40.0"
+ "@rollup/rollup-linux-riscv64-musl" "4.40.0"
+ "@rollup/rollup-linux-s390x-gnu" "4.40.0"
+ "@rollup/rollup-linux-x64-gnu" "4.40.0"
+ "@rollup/rollup-linux-x64-musl" "4.40.0"
+ "@rollup/rollup-win32-arm64-msvc" "4.40.0"
+ "@rollup/rollup-win32-ia32-msvc" "4.40.0"
+ "@rollup/rollup-win32-x64-msvc" "4.40.0"
+ fsevents "~2.3.2"
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
version "5.2.1"
@@ -3760,6 +4181,11 @@ shelljs@^0.8.4:
interpret "^1.0.0"
rechoir "^0.6.2"
+siginfo@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
+ integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
+
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -3787,6 +4213,16 @@ srcset@4:
resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4"
integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
+stackback@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
+ integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
+
+std-env@^3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
+ integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
+
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -3897,6 +4333,39 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
+tinybench@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
+ integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
+
+tinyexec@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
+ integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
+
+tinyglobby@^0.2.12, tinyglobby@^0.2.13:
+ version "0.2.13"
+ resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e"
+ integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==
+ dependencies:
+ fdir "^6.4.4"
+ picomatch "^4.0.2"
+
+tinypool@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2"
+ integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==
+
+tinyrainbow@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294"
+ integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==
+
+tinyspy@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
+ integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -3998,15 +4467,57 @@ utility-types@^3.10.0:
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
-uvu@^0.5.6:
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
- integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
- dependencies:
- dequal "^2.0.0"
- diff "^5.0.0"
- kleur "^4.0.3"
- sade "^1.7.3"
+vite-node@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.2.tgz#b17869a12307f5260b20ba4b58cf493afee70aa7"
+ integrity sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==
+ dependencies:
+ cac "^6.7.14"
+ debug "^4.4.0"
+ es-module-lexer "^1.6.0"
+ pathe "^2.0.3"
+ vite "^5.0.0 || ^6.0.0"
+
+"vite@^5.0.0 || ^6.0.0":
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.2.tgz#4c1bb01b1cea853686a191657bbc14272a038f0a"
+ integrity sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==
+ dependencies:
+ esbuild "^0.25.0"
+ fdir "^6.4.3"
+ picomatch "^4.0.2"
+ postcss "^8.5.3"
+ rollup "^4.34.9"
+ tinyglobby "^0.2.12"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+vitest@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.1.2.tgz#63afc16b6da3bea6e39f5387d80719e70634ba66"
+ integrity sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==
+ dependencies:
+ "@vitest/expect" "3.1.2"
+ "@vitest/mocker" "3.1.2"
+ "@vitest/pretty-format" "^3.1.2"
+ "@vitest/runner" "3.1.2"
+ "@vitest/snapshot" "3.1.2"
+ "@vitest/spy" "3.1.2"
+ "@vitest/utils" "3.1.2"
+ chai "^5.2.0"
+ debug "^4.4.0"
+ expect-type "^1.2.1"
+ magic-string "^0.30.17"
+ pathe "^2.0.3"
+ std-env "^3.9.0"
+ tinybench "^2.9.0"
+ tinyexec "^0.3.2"
+ tinyglobby "^0.2.13"
+ tinypool "^1.0.2"
+ tinyrainbow "^2.0.0"
+ vite "^5.0.0 || ^6.0.0"
+ vite-node "3.1.2"
+ why-is-node-running "^2.3.0"
w3c-keyname@^2.2.4:
version "2.2.8"
@@ -4055,6 +4566,14 @@ which@^1.2.9:
dependencies:
isexe "^2.0.0"
+why-is-node-running@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
+ integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
+ dependencies:
+ siginfo "^2.0.0"
+ stackback "0.0.2"
+
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"