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,