diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b774988c..81ab377b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,9 @@ jobs: - name: Test run: pnpm run test + - name: Integration Tests + run: pnpm run test:integrations + - name: Install Playwright Browsers run: npx playwright install --with-deps diff --git a/integrations/package.json b/integrations/package.json new file mode 100644 index 000000000..900679dc5 --- /dev/null +++ b/integrations/package.json @@ -0,0 +1,10 @@ +{ + "name": "internal-integrations", + "version": "0.0.0", + "private": true, + "devDependencies": { + "dedent": "1.5.3", + "fast-glob": "^3.3.2", + "kill-port": "^2.0.1" + } +} diff --git a/integrations/utils.ts b/integrations/utils.ts new file mode 100644 index 000000000..e601353b1 --- /dev/null +++ b/integrations/utils.ts @@ -0,0 +1,295 @@ +import dedent from 'dedent' +import fastGlob from 'fast-glob' +import killPort from 'kill-port' +import { execSync, spawn } from 'node:child_process' +import fs from 'node:fs/promises' +import net from 'node:net' +import { homedir, platform, tmpdir } from 'node:os' +import path from 'node:path' +import { test as defaultTest } from 'vitest' + +export let css = dedent +export let html = dedent +export let ts = dedent +export let json = dedent + +const REPO_ROOT = path.join(__dirname, '..') + +interface SpawnedProcess { + dispose: () => void + onStdout: (predicate: (message: string) => boolean) => Promise + onStderr: (predicate: (message: string) => boolean) => Promise +} + +interface TestConfig { + fs: { + [filePath: string]: string + } +} +interface TestContext { + exec(command: string): Promise + spawn(command: string): Promise + getFreePort(): Promise + fs: { + write(filePath: string, content: string): Promise + glob(pattern: string): Promise<[string, string][]> + } +} +type TestCallback = (context: TestContext) => Promise | void + +type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void } + +export function test( + name: string, + config: TestConfig, + testCallback: TestCallback, + { only = false } = {}, +) { + return (only ? defaultTest.only : defaultTest)(name, { timeout: 30000 }, async (options) => { + let root = await fs.mkdtemp( + // On Windows CI, tmpdir returns a path containing a weird RUNNER~1 folder + // that apparently causes the vite builds to not work. + path.join( + process.env.CI && platform() === 'win32' ? homedir() : tmpdir(), + 'tailwind-integrations', + ), + ) + + async function write(filename: string, content: string): Promise { + let full = path.join(root, filename) + + if (filename.endsWith('package.json')) { + content = overwriteVersionsInPackageJson(content) + } + + // Ensure that files written on Windows use \r\n line ending + if (platform() === 'win32') { + content = content.replace(/\n/g, '\r\n') + } + + let dir = path.dirname(full) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(full, content) + } + + for (let [filename, content] of Object.entries(config.fs)) { + await write(filename, content) + } + + try { + execSync('pnpm install', { cwd: root }) + } catch (error: any) { + console.error(error.stdout.toString()) + console.error(error.stderr.toString()) + throw error + } + + let disposables: (() => Promise)[] = [] + async function dispose() { + await Promise.all(disposables.map((dispose) => dispose())) + await fs.rm(root, { recursive: true, maxRetries: 3, force: true }) + } + options.onTestFinished(dispose) + + let context = { + async exec(command: string) { + return execSync(command, { cwd: root }).toString() + }, + async spawn(command: string) { + let resolveDisposal: (() => void) | undefined + let rejectDisposal: ((error: Error) => void) | undefined + let disposePromise = new Promise((resolve, reject) => { + resolveDisposal = resolve + rejectDisposal = reject + }) + + let child = spawn(command, { + cwd: root, + shell: true, + env: { + ...process.env, + }, + }) + + function dispose() { + child.kill() + + let timer = setTimeout( + () => rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)), + 1000, + ) + disposePromise.finally(() => clearTimeout(timer)) + return disposePromise + } + disposables.push(dispose) + function onExit() { + resolveDisposal?.() + } + + let stdoutMessages: string[] = [] + let stderrMessages: string[] = [] + + let stdoutActors: SpawnActor[] = [] + let stderrActors: SpawnActor[] = [] + + function notifyNext(actors: SpawnActor[], messages: string[]) { + if (actors.length <= 0) return + let [next] = actors + + for (let [idx, message] of messages.entries()) { + if (next.predicate(message)) { + messages.splice(0, idx + 1) + let actorIdx = actors.indexOf(next) + actors.splice(actorIdx, 1) + next.resolve() + break + } + } + } + + child.stdout.on('data', (result) => { + stdoutMessages.push(result.toString()) + notifyNext(stdoutActors, stdoutMessages) + }) + child.stderr.on('data', (result) => { + stderrMessages.push(result.toString()) + notifyNext(stderrActors, stderrMessages) + }) + child.on('exit', onExit) + child.on('error', (error) => { + if (error.name !== 'AbortError') { + throw error + } + }) + + options.onTestFailed(() => { + stdoutMessages.map((message) => console.log(message)) + stderrMessages.map((message) => console.error(message)) + }) + + return { + dispose, + onStdout(predicate: (message: string) => boolean) { + return new Promise((resolve) => { + stdoutActors.push({ predicate, resolve }) + notifyNext(stdoutActors, stdoutMessages) + }) + }, + onStderr(predicate: (message: string) => boolean) { + return new Promise((resolve) => { + stderrActors.push({ predicate, resolve }) + notifyNext(stderrActors, stderrMessages) + }) + }, + } + }, + async getFreePort(): Promise { + return new Promise((resolve, reject) => { + let server = net.createServer() + server.listen(0, () => { + let address = server.address() + let port = address === null || typeof address === 'string' ? null : address.port + + server.close(() => { + if (port === null) { + reject(new Error(`Failed to get a free port: address is ${address}`)) + } else { + disposables.push(async () => { + // Wait for 10ms in case the process was just killed + await new Promise((resolve) => setTimeout(resolve, 10)) + + // kill-port uses `lsof` on macOS which is expensive and can + // block for multiple seconds. In order to avoid that for a + // server that is no longer running, we check if the port is + // still in use first. + let isPortTaken = await testIfPortTaken(port) + if (!isPortTaken) { + return + } + + await killPort(port) + }) + resolve(port) + } + }) + }) + }) + }, + fs: { + write, + async glob(pattern: string) { + let files = await fastGlob(pattern, { cwd: root }) + return Promise.all( + files.map(async (file) => { + let content = await fs.readFile(path.join(root, file), 'utf8') + return [file, content] + }), + ) + }, + }, + } satisfies TestContext + + await testCallback(context) + }) +} +test.only = (name: string, config: TestConfig, testCallback: TestCallback) => { + return test(name, config, testCallback, { only: true }) +} + +// Maps package names to their tarball filenames. See scripts/pack-packages.ts +// for more details. +function pkgToFilename(name: string) { + return `${name.replace('@', '').replace('/', '-')}.tgz` +} + +function overwriteVersionsInPackageJson(content: string): string { + let json = JSON.parse(content) + + // Resolve all workspace:^ versions to local tarballs + ;['dependencies', 'devDependencies', 'peerDependencies'].forEach((key) => { + let dependencies = json[key] || {} + for (let dependency in dependencies) { + if (dependencies[dependency] === 'workspace:^') { + dependencies[dependency] = resolveVersion(dependency) + } + } + }) + + // Inject transitive dependency overwrite. This is necessary because + // @tailwindcss/vite internally depends on a specific version of + // @tailwindcss/oxide and we instead want to resolve it to the locally built + // version. + json.pnpm ||= {} + json.pnpm.overrides ||= {} + json.pnpm.overrides['@tailwindcss/oxide'] = resolveVersion('@tailwindcss/oxide') + + return JSON.stringify(json, null, 2) +} + +function resolveVersion(dependency: string) { + let tarball = path.join(REPO_ROOT, 'dist', pkgToFilename(dependency)) + return `file:${tarball}` +} + +export function stripTailwindComment(content: string) { + return content.replace(/\/\*! tailwindcss .*? \*\//g, '').trim() +} + +function testIfPortTaken(port: number): Promise { + return new Promise((resolve) => { + let client = new net.Socket() + client.once('connect', () => { + resolve(true) + client.end() + }) + client.once('error', (error: any) => { + if (error.code !== 'ECONNREFUSED') { + resolve(true) + } else { + resolve(false) + } + client.end() + }) + client.connect({ port: port, host: 'localhost' }) + }) +} diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts new file mode 100644 index 000000000..56552b0ad --- /dev/null +++ b/integrations/vite/index.test.ts @@ -0,0 +1,157 @@ +import { expect } from 'vitest' +import { css, html, json, stripTailwindComment, test, ts } from '../utils' + +async function fetchCSS(pathname: string, port: number) { + // We need to fetch the main index.html file to populate the list of + // candidates. + let body = await fetch(`http://localhost:${port}`) + // Make sure the main request is garbage collected. + body.blob() + + let response = await fetch(`http://localhost:${port}${pathname}`, { + headers: { + Accept: 'text/css', + }, + }) + return response.text() +} + +test( + 'works with production builds', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import 'tailwindcss/theme' reference; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ fs, exec }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, content] = files[0] + expect(stripTailwindComment(content)).toMatchInlineSnapshot( + ` + ".m-2 { + margin: var(--spacing-2, .5rem); + } + + .underline { + text-decoration-line: underline; + }" + `, + ) + }, +) + +test( + 'works with dev builds and live reloads', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^5.3.5" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
Hello, world!
+ + `, + 'src/index.css': css` + @import 'tailwindcss/theme' reference; + @import 'tailwindcss/utilities'; + `, + }, + }, + async ({ spawn, getFreePort, fs }) => { + let port = await getFreePort() + let process = await spawn(`pnpm vite dev --port ${port}`) + + await process.onStdout((message) => message.includes('ready in')) + + let css = await fetchCSS('/src/index.css', port) + expect(stripTailwindComment(css)).toMatchInlineSnapshot( + ` + ".underline { + text-decoration-line: underline; + }" + `, + ) + + await fs.write( + 'index.html', + html` + + + + +
Hello, world!
+ + `, + ) + await process.onStdout((message) => message.includes('page reload')) + + css = await fetchCSS('/src/index.css', port) + expect(stripTailwindComment(css)).toMatchInlineSnapshot( + ` + ".m-2 { + margin: var(--spacing-2, 0.5rem); + } + .underline { + text-decoration-line: underline; + }" + `, + ) + }, +) diff --git a/package.json b/package.json index 85c86202e..56879d91a 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,27 @@ "options": { "parser": "jsonc" } + }, + { + "files": [ + "integrations/**/*.ts" + ], + "options": { + "plugins": [ + "prettier-plugin-embed", + "prettier-plugin-organize-imports" + ] + } } ] }, "scripts": { "format": "prettier --write .", "lint": "prettier --check . && turbo lint", - "build": "turbo build --filter=!./playgrounds/*", + "build": "turbo build --filter=!./playgrounds/* && node ./scripts/pack-packages.mjs", "dev": "turbo dev --filter=!./playgrounds/*", "test": "cargo test && vitest run", + "test:integrations": "vitest --root=./integrations", "test:ui": "pnpm run --filter=tailwindcss test:ui", "tdd": "vitest", "bench": "vitest bench", @@ -40,6 +52,7 @@ "postcss": "8.4.24", "postcss-import": "^16.1.0", "prettier": "^3.2.5", + "prettier-plugin-embed": "^0.4.15", "prettier-plugin-organize-imports": "^3.2.4", "tsup": "^8.0.2", "turbo": "^1.13.3", diff --git a/packages/tailwindcss/vitest.config.ts b/packages/tailwindcss/vitest.config.ts index d819d1bad..880f8aaa9 100644 --- a/packages/tailwindcss/vitest.config.ts +++ b/packages/tailwindcss/vitest.config.ts @@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - exclude: ['**/*.spec.?(c|m)[jt]s?(x)'], + exclude: ['**/*.spec.?(c|m)[jt]s?(x)', 'integrations/**/*'], }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aad0d452b..888eee519 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: prettier: specifier: ^3.2.5 version: 3.3.3 + prettier-plugin-embed: + specifier: ^0.4.15 + version: 0.4.15 prettier-plugin-organize-imports: specifier: ^3.2.4 version: 3.2.4(prettier@3.3.3)(typescript@5.5.4) @@ -107,6 +110,18 @@ importers: crates/node/npm/win32-x64-msvc: {} + integrations: + devDependencies: + dedent: + specifier: 1.5.3 + version: 1.5.3 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + kill-port: + specifier: ^2.0.1 + version: 2.0.1 + packages/@tailwindcss-cli: dependencies: '@parcel/watcher': @@ -1204,7 +1219,6 @@ packages: bun@1.1.21: resolution: {integrity: sha512-mvqYEvafGskIVTjlftbKvsXtyR6z/SQnhJsVw0xCU46pc56oX1sAGvaemWKOy/sy/gGMHcgLE0KUidDQQzqXWQ==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -1322,6 +1336,14 @@ packages: supports-color: optional: true + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1566,6 +1588,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1630,6 +1656,9 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} + get-them-args@1.3.2: + resolution: {integrity: sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==} + get-tsconfig@4.7.6: resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} @@ -1923,6 +1952,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kill-port@2.0.1: + resolution: {integrity: sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==} + hasBin: true + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2036,6 +2069,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micro-memoize@4.1.2: + resolution: {integrity: sha512-+HzcV2H+rbSJzApgkj0NdTakkC+bnyeiUxgT6/m7mjcz1CmM22KYFKp+EVj1sWe4UYcnriJr5uqHQD/gMHLD+g==} + micromatch@4.0.7: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} @@ -2185,6 +2221,10 @@ packages: package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + package-up@5.0.0: + resolution: {integrity: sha512-MQEgDUvXCa3sGvqHg3pzHO8e9gqTCMPVrWUko3vPQGntwegmFo52mZb2abIVTjFnUcW0BcPz0D93jV5Cas1DWA==} + engines: {node: '>=18'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2295,6 +2335,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-embed@0.4.15: + resolution: {integrity: sha512-9pZVIp3bw2jw+Ge+iAMZ4j+sIVC9cPruZ93H2tj5Wa/3YDFDJ/uYyVWdUGfcFUnv28drhW2Bmome9xSGXsPKOw==} + prettier-plugin-organize-imports@3.2.4: resolution: {integrity: sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==} peerDependencies: @@ -2426,6 +2469,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-exec@1.0.2: + resolution: {integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} @@ -2567,6 +2613,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-jsonc@1.0.1: + resolution: {integrity: sha512-ik6BCxzva9DoiEfDX/li0L2cWKPPENYvixUprFdl3YPi4bZZUhDnNI9YUkacrv+uIG90dnxR5mNqaoD6UhD6Bw==} + tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} @@ -2673,6 +2722,10 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + type-fest@4.23.0: + resolution: {integrity: sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==} + engines: {node: '>=16'} + typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -3752,6 +3805,8 @@ snapshots: dependencies: ms: 2.1.2 + dedent@1.5.3: {} + deep-eql@5.0.2: {} deep-equal@2.2.3: @@ -3987,7 +4042,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4011,7 +4066,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4033,7 +4088,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -4228,6 +4283,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-up-simple@1.0.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4291,6 +4348,8 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 + get-them-args@1.3.2: {} + get-tsconfig@4.7.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -4578,6 +4637,11 @@ snapshots: dependencies: json-buffer: 3.0.1 + kill-port@2.0.1: + dependencies: + get-them-args: 1.3.2 + shell-exec: 1.0.2 + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -4666,6 +4730,8 @@ snapshots: merge2@1.4.1: {} + micro-memoize@4.1.2: {} + micromatch@4.0.7: dependencies: braces: 3.0.3 @@ -4822,6 +4888,10 @@ snapshots: package-json-from-dist@1.0.0: {} + package-up@5.0.0: + dependencies: + find-up-simple: 1.0.0 + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4900,6 +4970,17 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-embed@0.4.15: + dependencies: + '@types/estree': 1.0.5 + dedent: 1.5.3 + micro-memoize: 4.1.2 + package-up: 5.0.0 + tiny-jsonc: 1.0.1 + type-fest: 4.23.0 + transitivePeerDependencies: + - babel-plugin-macros + prettier-plugin-organize-imports@3.2.4(prettier@3.3.3)(typescript@5.5.4): dependencies: prettier: 3.3.3 @@ -5049,6 +5130,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-exec@1.0.2: {} + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -5191,6 +5274,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-jsonc@1.0.1: {} + tinybench@2.8.0: {} tinypool@1.0.0: {} @@ -5286,6 +5371,8 @@ snapshots: type-fest@0.20.2: {} + type-fest@4.23.0: {} + typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5e7582145..4c69550e7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - 'crates/node/npm/*' - 'packages/*' - 'playgrounds/*' + - 'integrations' catalog: vite: ^5.2.11 diff --git a/scripts/pack-packages.mjs b/scripts/pack-packages.mjs new file mode 100644 index 000000000..8791f39c4 --- /dev/null +++ b/scripts/pack-packages.mjs @@ -0,0 +1,67 @@ +import { exec, execSync } from 'node:child_process' +import fs from 'node:fs/promises' +import { platform } from 'node:os' +import path, { dirname } from 'node:path' +import url from 'node:url' + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) +let root = path.resolve(__dirname, '..') + +let command = platform() === 'win32' ? 'cd' : 'pwd' +let rawPaths = execSync(`pnpm --silent --filter=!./playgrounds/* -r exec ${command}`).toString() + +let paths = rawPaths + .trim() + .split(/\r?\n/) + .map((x) => path.join(x, 'package.json')) + +let workspaces = new Map() + +// Track all the workspaces +for (let path of paths) { + let pkg = await fs.readFile(path, 'utf8').then(JSON.parse) + if (pkg.private) continue + workspaces.set(pkg.name, { version: pkg.version ?? '', dir: dirname(path) }) +} + +// Clean dist folder +await fs.rm(path.join(root, 'dist'), { recursive: true, force: true }) + +Promise.all( + [...workspaces.entries()].map(async ([name, { version, dir }]) => { + function pack() { + return new Promise((resolve) => { + exec( + `pnpm pack --pack-destination="${path.join(root, 'dist').replace(/\\/g, '\\\\')}"`, + { cwd: dir }, + (err, stdout, stderr) => { + if (err) { + console.error(err, stdout, stderr) + } + + resolve(lastLine(stdout.trim())) + }, + ) + }) + } + + let filename = await pack() + // Remove version suffix + await fs.rename( + path.join(root, 'dist', path.basename(filename)), + path.join(root, 'dist', pkgToFilename(name)), + ) + }), +).then(() => { + console.log('Done.') +}) + +function pkgToFilename(name) { + return `${name.replace('@', '').replace('/', '-')}.tgz` +} + +function lastLine(str) { + let index = str.lastIndexOf('\n') + if (index === -1) return str + return str.slice(index + 1) +}