diff --git a/.changeset/eleven-oranges-collect.md b/.changeset/eleven-oranges-collect.md
new file mode 100644
index 000000000..eb1e3c9dc
--- /dev/null
+++ b/.changeset/eleven-oranges-collect.md
@@ -0,0 +1,8 @@
+---
+'@vanilla-extract/webpack-plugin': patch
+---
+
+Fixes a bug that was causing style compilation to fail on paths containing [webpack template strings] such as `[id]` or [Next.js dynamic routes] such as `[slug]`.
+
+[webpack template strings]: https://webpack.js.org/configuration/output/#template-strings
+[next.js dynamic routes]: https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
diff --git a/fixtures/template-string-paths/index.html b/fixtures/template-string-paths/index.html
new file mode 100644
index 000000000..4ede1709c
--- /dev/null
+++ b/fixtures/template-string-paths/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/fixtures/template-string-paths/package.json b/fixtures/template-string-paths/package.json
new file mode 100644
index 000000000..74d0146cc
--- /dev/null
+++ b/fixtures/template-string-paths/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@fixtures/template-string-paths",
+ "version": "0.0.1",
+ "main": "src/index.ts",
+ "sideEffects": true,
+ "author": "SEEK",
+ "private": true,
+ "dependencies": {
+ "@vanilla-extract/css": "1.14.1"
+ }
+}
diff --git a/fixtures/template-string-paths/src/[...slug]/index.css.ts b/fixtures/template-string-paths/src/[...slug]/index.css.ts
new file mode 100644
index 000000000..f25e11d5b
--- /dev/null
+++ b/fixtures/template-string-paths/src/[...slug]/index.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css';
+
+export const catchAllSegment = style({ color: 'lime' });
diff --git a/fixtures/template-string-paths/src/[[...slug]]/index.css.ts b/fixtures/template-string-paths/src/[[...slug]]/index.css.ts
new file mode 100644
index 000000000..193a5e36e
--- /dev/null
+++ b/fixtures/template-string-paths/src/[[...slug]]/index.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css';
+
+export const optionalCatchAllSegment = style({ color: 'orchid' });
diff --git a/fixtures/template-string-paths/src/[[id]]/index.css.ts b/fixtures/template-string-paths/src/[[id]]/index.css.ts
new file mode 100644
index 000000000..70a8e9d58
--- /dev/null
+++ b/fixtures/template-string-paths/src/[[id]]/index.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css';
+
+export const doubleSquareBracketId = style({ color: 'darkkhaki' });
diff --git a/fixtures/template-string-paths/src/[]/index.css.ts b/fixtures/template-string-paths/src/[]/index.css.ts
new file mode 100644
index 000000000..5e179bac2
--- /dev/null
+++ b/fixtures/template-string-paths/src/[]/index.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css';
+
+export const emptySquareBrackets = style({ color: 'blue' });
diff --git a/fixtures/template-string-paths/src/[id]/index.css.ts b/fixtures/template-string-paths/src/[id]/index.css.ts
new file mode 100644
index 000000000..abdf7c0dc
--- /dev/null
+++ b/fixtures/template-string-paths/src/[id]/index.css.ts
@@ -0,0 +1,3 @@
+import { style } from '@vanilla-extract/css';
+
+export const singleSquareBracketsId = style({ color: 'tomato' });
diff --git a/fixtures/template-string-paths/src/index.ts b/fixtures/template-string-paths/src/index.ts
new file mode 100644
index 000000000..c70358ac9
--- /dev/null
+++ b/fixtures/template-string-paths/src/index.ts
@@ -0,0 +1,20 @@
+import { emptySquareBrackets } from './[]/index.css';
+import { singleSquareBracketsId } from './[id]/index.css';
+import { doubleSquareBracketId } from './[[id]]/index.css';
+import { catchAllSegment } from './[...slug]/index.css';
+import { optionalCatchAllSegment } from './[[...slug]]/index.css';
+
+// Fixture for testing escaping of webpack template strings and Next.js dyanmic routes
+// https://webpack.js.org/configuration/output/#template-strings
+// https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
+function render() {
+ document.body.innerHTML = `
+ [] path
+ [id] path
+ [[id]] path
+ [...slug] path
+ [[...slug]] path
+ `;
+}
+
+render();
diff --git a/packages/webpack-plugin/src/__snapshots__/compiler.test.ts.snap b/packages/webpack-plugin/src/__snapshots__/compiler.test.ts.snap
new file mode 100644
index 000000000..1ca8f4787
--- /dev/null
+++ b/packages/webpack-plugin/src/__snapshots__/compiler.test.ts.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`escapeWebpackTemplateString() /some/path/[...slug].js pattern 1`] = `"/some/path/[...slug].js"`;
+
+exports[`escapeWebpackTemplateString() /some/path/[[...slug]]/index.js pattern 1`] = `"/some/path/[[...slug]]/index.js"`;
+
+exports[`escapeWebpackTemplateString() /some/path[]/[slug]/[[foo]]/index.js pattern 1`] = `"/some/path[]/[\\slug\\]/[[\\foo\\]]/index.js"`;
diff --git a/packages/webpack-plugin/src/compiler.test.ts b/packages/webpack-plugin/src/compiler.test.ts
new file mode 100644
index 000000000..82d845885
--- /dev/null
+++ b/packages/webpack-plugin/src/compiler.test.ts
@@ -0,0 +1,11 @@
+import { escapeWebpackTemplateString } from './compiler';
+
+describe('escapeWebpackTemplateString()', () => {
+ test.each([
+ '/some/path/[...slug].js',
+ '/some/path/[[...slug]]/index.js',
+ '/some/path[]/[slug]/[[foo]]/index.js',
+ ])('%s pattern', (filePath) => {
+ expect(escapeWebpackTemplateString(filePath)).toMatchSnapshot();
+ });
+});
diff --git a/packages/webpack-plugin/src/compiler.ts b/packages/webpack-plugin/src/compiler.ts
index dd6e7c9e9..59be9c44c 100644
--- a/packages/webpack-plugin/src/compiler.ts
+++ b/packages/webpack-plugin/src/compiler.ts
@@ -55,6 +55,11 @@ function getRootCompilation(loader: LoaderContext) {
return compilation;
}
+const templateStringRegexp = /\[([^\[\]\.]+)\]/g;
+
+export const escapeWebpackTemplateString = (s: string) =>
+ s.replaceAll(templateStringRegexp, '[\\$1\\]');
+
function compileVanillaSource(
loader: LoaderContext,
externals: Externals | undefined,
@@ -64,9 +69,15 @@ function compileVanillaSource(
loader._compiler.webpack && loader._compiler.webpack.version,
);
const compat = createCompat(isWebpack5);
- // Child compiler will compile vanilla-extract files to be evaled during compilation
- const outputOptions = { filename: loader.resourcePath };
+ // Escape webpack template strings and Next.js dynamic routes in output files so they don't get replaced
+ // Non-standard escape syntax, see https://webpack.js.org/configuration/output/#template-strings
+ // and https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes
+ const outputOptions = {
+ filename: escapeWebpackTemplateString(loader.resourcePath),
+ };
+
+ // Child compiler will compile vanilla-extract files to be evaled during compilation
const compilerName = getCompilerName(loader.resourcePath);
const childCompiler = getRootCompilation(loader).createChildCompiler(
compilerName,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a587263c..edce7ef5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -220,6 +220,12 @@ importers:
specifier: 1.6.1
version: link:../../packages/sprinkles
+ fixtures/template-string-paths:
+ dependencies:
+ '@vanilla-extract/css':
+ specifier: 1.14.1
+ version: link:../../packages/css
+
fixtures/themed:
dependencies:
'@vanilla-extract/css':
@@ -682,6 +688,9 @@ importers:
'@fixtures/sprinkles':
specifier: '*'
version: link:../fixtures/sprinkles
+ '@fixtures/template-string-paths':
+ specifier: '*'
+ version: link:../fixtures/template-string-paths
'@fixtures/themed':
specifier: '*'
version: link:../fixtures/themed
diff --git a/test-helpers/package.json b/test-helpers/package.json
index 036eb5d9a..97ec67182 100644
--- a/test-helpers/package.json
+++ b/test-helpers/package.json
@@ -12,6 +12,7 @@
"@fixtures/low-level": "*",
"@fixtures/recipes": "*",
"@fixtures/sprinkles": "*",
+ "@fixtures/template-string-paths": "*",
"@fixtures/themed": "*",
"@fixtures/thirdparty": "*",
"@fixtures/unused-modules": "*",
diff --git a/tests/e2e/template-string-paths.playwright.ts b/tests/e2e/template-string-paths.playwright.ts
new file mode 100644
index 000000000..621bc02ee
--- /dev/null
+++ b/tests/e2e/template-string-paths.playwright.ts
@@ -0,0 +1,43 @@
+import { expect } from '@playwright/test';
+import {
+ getStylesheet,
+ startFixture,
+ TestServer,
+} from '@vanilla-extract-private/test-helpers';
+
+import test from './fixture';
+import { webpack as testCases } from './testCases';
+
+testCases.forEach(({ type, mode, snapshotCss = true }) => {
+ test.describe(`template-string-paths - ${type} (${mode})`, () => {
+ let server: TestServer;
+
+ test.beforeAll(async ({ port }) => {
+ server = await startFixture('template-string-paths', {
+ type,
+ mode,
+ basePort: port,
+ });
+ });
+
+ test('screenshot', async ({ page }) => {
+ await page.goto(server.url);
+
+ expect(await page.screenshot()).toMatchSnapshot(
+ 'template-string-paths.png',
+ );
+ });
+
+ if (snapshotCss) {
+ test('CSS @agnostic', async () => {
+ expect(
+ await getStylesheet(server.url, server.stylesheet),
+ ).toMatchSnapshot(`template-string-paths-${type}--${mode}.css`);
+ });
+ }
+
+ test.afterAll(async () => {
+ await server.close();
+ });
+ });
+});
diff --git a/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Desktop---Chromium-darwin.png b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Desktop---Chromium-darwin.png
new file mode 100644
index 000000000..bb37654b7
Binary files /dev/null and b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Desktop---Chromium-darwin.png differ
diff --git a/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Mobile---Chromium-darwin.png b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Mobile---Chromium-darwin.png
new file mode 100644
index 000000000..d0dc710b6
Binary files /dev/null and b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-Mobile---Chromium-darwin.png differ
diff --git a/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--development.css b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--development.css
new file mode 100644
index 000000000..ea948545d
--- /dev/null
+++ b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--development.css
@@ -0,0 +1,15 @@
+.\[\]_emptySquareBrackets__13abg9g0 {
+ color: blue;
+}
+.\[id\]_singleSquareBracketsId__1d2wsrw0 {
+ color: tomato;
+}
+.\[\[id\]\]_doubleSquareBracketId__1aosxxv0 {
+ color: darkkhaki;
+}
+.\[\.\.\.slug\]_catchAllSegment__169etlp0 {
+ color: lime;
+}
+.\[\[\.\.\.slug\]\]_optionalCatchAllSegment__1kvknas0 {
+ color: orchid;
+}
diff --git a/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--production.css b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--production.css
new file mode 100644
index 000000000..8327813bf
--- /dev/null
+++ b/tests/e2e/template-string-paths.playwright.ts-snapshots/template-string-paths-mini-css-extract--production.css
@@ -0,0 +1,15 @@
+._13abg9g0 {
+ color: blue;
+}
+._1d2wsrw0 {
+ color: tomato;
+}
+._1aosxxv0 {
+ color: darkkhaki;
+}
+._169etlp0 {
+ color: lime;
+}
+._1kvknas0 {
+ color: orchid;
+}
diff --git a/tests/e2e/testCases.ts b/tests/e2e/testCases.ts
index 5c698d331..6f7486ebc 100644
--- a/tests/e2e/testCases.ts
+++ b/tests/e2e/testCases.ts
@@ -1,7 +1,11 @@
-export const all = [
+export const webpack = [
{ type: 'mini-css-extract', mode: 'development', snapshotCss: true },
{ type: 'mini-css-extract', mode: 'production', snapshotCss: true },
{ type: 'style-loader', mode: 'development', snapshotCss: false },
+] as const;
+
+export const all = [
+ ...webpack,
{ type: 'esbuild', mode: 'development', snapshotCss: true },
{ type: 'esbuild', mode: 'production', snapshotCss: true },
{ type: 'esbuild-runtime', mode: 'development', snapshotCss: false },
diff --git a/tsconfig.json b/tsconfig.json
index f879ab2c7..a3a82483f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
- "lib": ["es2019", "es2017", "dom"],
+ "lib": ["es2021", "dom"],
"noEmit": true,
"noImplicitAny": true,
"noUnusedLocals": true,