From 488d7885cafcc6d5b5f775c0cf9ae5c63425b308 Mon Sep 17 00:00:00 2001 From: Aaron Date: Thu, 20 Jul 2023 22:54:30 -0500 Subject: [PATCH] feat: Windows support (#50) --- .github/workflows/release.yml | 4 +-- .github/workflows/validate.yml | 11 +++++--- docs/get-started/installation.md | 6 +++++ e2e/utils.ts | 3 ++- .../build/__tests__/findEntrypoints.test.ts | 8 +++++- src/core/build/generateTypesDir.ts | 27 +++++++++---------- src/core/utils/detectDevChanges.ts | 6 +++-- src/core/utils/entrypoints.ts | 9 ++++--- src/core/utils/importTsFile.ts | 6 ++++- src/core/utils/paths.ts | 17 ++++++++++++ src/core/utils/public.ts | 4 ++- src/core/vite-plugins/multipageMove.ts | 5 ++-- src/core/vite-plugins/virtualEntrypoint.ts | 3 ++- src/index.ts | 5 ++++ tsconfig.base.json | 2 +- 15 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 src/core/utils/paths.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11981f50..3eb2f9cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ jobs: uses: './.github/workflows/validate.yml' publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: - validate steps: @@ -31,7 +31,7 @@ jobs: run: | pnpm install --ignore-scripts pnpm build - pnpm install + pnpm install --ignore-scripts - name: Bump and Tag run: | diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 973d5d45..cc0e6027 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,7 +9,11 @@ on: jobs: wxt: name: WXT - runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v3 @@ -29,7 +33,8 @@ jobs: run: | pnpm install --ignore-scripts pnpm build - pnpm install + pnpm install --ignore-scripts + cd demo && pnpm wxt prepare - name: Formatting run: pnpm format:check @@ -52,7 +57,7 @@ jobs: project-templates: name: Project Templates - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v3 diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md index f9a36a1c..6b609850 100644 --- a/docs/get-started/installation.md +++ b/docs/get-started/installation.md @@ -103,6 +103,12 @@ Finally, add scripts to your `package.json`: > You can skip `*:firefox` scripts if you don't want to support Firefox +## 🚧 WSL Support + +**_WXT does not support [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/) yet_**. See [Issue #55](https://github.com/aklinker1/wxt/issues/55) to track progress. + +In the meantime, you can use `cmd` instead. + ## Development Once you've installed WXT, you can start the development server using the `dev` script. diff --git a/e2e/utils.ts b/e2e/utils.ts index d42bb8af..e5e1c464 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import glob from 'fast-glob'; import { execSync } from 'child_process'; import { InlineConfig, UserConfig, build } from '../src'; +import { normalizePath } from '../src/core/utils/paths'; export class TestProject { files: Array<[string, string]> = []; @@ -113,7 +114,7 @@ export class TestProject { async serializeFile(path: string): Promise { const absolutePath = resolve(this.root, path); return [ - relative(this.root, absolutePath), + normalizePath(relative(this.root, absolutePath)), await fs.readFile(absolutePath), ].join(`\n${''.padEnd(40, '-')}\n`); } diff --git a/src/core/build/__tests__/findEntrypoints.test.ts b/src/core/build/__tests__/findEntrypoints.test.ts index 6be591ae..03e5748c 100644 --- a/src/core/build/__tests__/findEntrypoints.test.ts +++ b/src/core/build/__tests__/findEntrypoints.test.ts @@ -12,6 +12,7 @@ import fs from 'fs-extra'; import { importTsFile } from '../../utils/importTsFile'; import glob from 'fast-glob'; import { fakeInternalConfig } from '../../../testing/fake-objects'; +import { unnormalizePath } from '../../utils/paths'; vi.mock('../../utils/importTsFile'); const importTsFileMock = vi.mocked(importTsFile); @@ -443,9 +444,14 @@ describe('findEntrypoints', () => { it('should not allow multiple entrypoints with the same name', async () => { globMock.mockResolvedValueOnce(['popup.html', 'popup/index.html']); + const expectedPaths = [ + 'src/entrypoints/popup.html', + 'src/entrypoints/popup/index.html', + ].map(unnormalizePath); await expect(() => findEntrypoints(config)).rejects.toThrowError( - 'Multiple entrypoints with the name "popup" detected, but only one is allowed: src/entrypoints/popup.html, src/entrypoints/popup/index.html', + 'Multiple entrypoints with the name "popup" detected, but only one is allowed: ' + + expectedPaths.join(', '), ); }); }); diff --git a/src/core/build/generateTypesDir.ts b/src/core/build/generateTypesDir.ts index c674c949..d6c1ebe7 100644 --- a/src/core/build/generateTypesDir.ts +++ b/src/core/build/generateTypesDir.ts @@ -6,6 +6,7 @@ import { getEntrypointBundlePath } from '../utils/entrypoints'; import { getUnimportOptions } from '../utils/auto-imports'; import { getGlobals } from '../utils/globals'; import { getPublicFiles } from '../utils/public'; +import { normalizePath } from '../utils/paths'; /** * Generate and write all the files inside the `InternalConfig.typesDir` directory. @@ -58,6 +59,7 @@ async function writePathsDeclarationFile( ), ) .concat(await getPublicFiles(config)) + .map(normalizePath) .map((path) => ` | "/${path}"`) .sort() .join('\n'); @@ -113,7 +115,8 @@ async function writeMainDeclarationFile( '// Generated by wxt', `/// `, ...references.map( - (ref) => `/// `, + (ref) => + `/// `, ), ].join('\n') + '\n', ); @@ -136,32 +139,26 @@ async function writeTsConfigFile( "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - - /* Type Checking */ "strict": true, "lib": ["DOM", "WebWorker"], - - /* Completeness */ "skipLibCheck": true, - - /* Aliases */ - "baseUrl": "${relative(dir, config.root)}", + "baseUrl": "${normalizePath(relative(dir, config.root))}", "paths": { "@@": ["."], "@@/*": ["./*"], "~~": ["."], "~~/*": ["./*"], - "@": ["${relative(config.root, config.srcDir)}"], - "@/*": ["${relative(config.root, config.srcDir)}/*"], - "~": ["${relative(config.root, config.srcDir)}"], - "~/*": ["${relative(config.root, config.srcDir)}/*"] + "@": ["${normalizePath(relative(config.root, config.srcDir))}"], + "@/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"], + "~": ["${normalizePath(relative(config.root, config.srcDir))}"], + "~/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"] } }, "include": [ - "${relative(dir, config.root)}/**/*", - "./${relative(dir, mainReference)}" + "${normalizePath(relative(dir, config.root))}/**/*", + "./${normalizePath(relative(dir, mainReference))}" ], - "exclude": ["${relative(dir, config.outBaseDir)}"] + "exclude": ["${normalizePath(relative(dir, config.outBaseDir))}"] }`, ); } diff --git a/src/core/utils/detectDevChanges.ts b/src/core/utils/detectDevChanges.ts index 80ac758e..6881bc9f 100644 --- a/src/core/utils/detectDevChanges.ts +++ b/src/core/utils/detectDevChanges.ts @@ -1,6 +1,7 @@ import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types'; import * as vite from 'vite'; import { every } from './arrays'; +import { normalizePath } from './paths'; /** * Compare the changed files vs the build output and determine what kind of reload needs to happen: @@ -102,15 +103,16 @@ function findEffectedSteps( currentOutput: BuildOutput, ): DetectedChange[] { const changes: DetectedChange[] = []; - const changedPath = changedFile[1]; + const changedPath = normalizePath(changedFile[1]); const isChunkEffected = ( chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset, ): boolean => // If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered - // TODO: use bundle path to support `/index.html`? + // fileName is normalized, relative bundle path (chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) || // If it's a chunk that depends on the changed file, it is effected + // moduleIds are absolute, normalized paths (chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath)); for (const step of currentOutput.steps) { diff --git a/src/core/utils/entrypoints.ts b/src/core/utils/entrypoints.ts index 9d9b303d..abad070e 100644 --- a/src/core/utils/entrypoints.ts +++ b/src/core/utils/entrypoints.ts @@ -1,5 +1,6 @@ import { Entrypoint } from '../types'; import path, { relative, resolve } from 'node:path'; +import { normalizePath } from './paths'; export function getEntrypointName( entrypointsDir: string, @@ -7,8 +8,8 @@ export function getEntrypointName( // type: Entrypoint['type'], ): string { const relativePath = path.relative(entrypointsDir, inputPath); - // Grab the string up to the first . or / - const name = relativePath.split(/[\.\/]/, 2)[0]; + // Grab the string up to the first . or / or \\ + const name = relativePath.split(/[\.\/\\]/, 2)[0]; return name; } @@ -29,5 +30,7 @@ export function getEntrypointBundlePath( outDir: string, ext: string, ): string { - return relative(outDir, getEntrypointOutputFile(entrypoint, ext)); + return normalizePath( + relative(outDir, getEntrypointOutputFile(entrypoint, ext)), + ); } diff --git a/src/core/utils/importTsFile.ts b/src/core/utils/importTsFile.ts index df150a35..5549cb75 100644 --- a/src/core/utils/importTsFile.ts +++ b/src/core/utils/importTsFile.ts @@ -6,6 +6,7 @@ import { resolve } from 'path'; import transform from 'jiti/dist/babel'; import { getUnimportOptions } from './auto-imports'; import { removeImportStatements } from './strings'; +import { normalizePath } from './paths'; /** * Get the value from the default export of a `path`. @@ -27,6 +28,8 @@ export async function importTsFile( config: InternalConfig, ): Promise { config.logger.debug('Loading file metadata:', path); + // JITI & Babel uses normalized paths. + const normalPath = normalizePath(path); const unimport = createUnimport({ ...getUnimportOptions(config), @@ -53,7 +56,8 @@ export async function importTsFile( ), }, transform(opts) { - if (opts.filename === path) return transform({ ...opts, source: code }); + if (opts.filename === normalPath) + return transform({ ...opts, source: code }); else return transform(opts); }, }); diff --git a/src/core/utils/paths.ts b/src/core/utils/paths.ts new file mode 100644 index 00000000..3640cfaa --- /dev/null +++ b/src/core/utils/paths.ts @@ -0,0 +1,17 @@ +import nodePath from 'node:path'; +import * as vite from 'vite'; + +/** + * Converts system paths to normalized bundler path. On windows and unix, this returns paths with / + * instead of \. + */ +export function normalizePath(path: string): string { + return vite.normalizePath(path); +} + +/** + * Given a normalized path, convert it to the system path style. On Windows, switch to \, otherwise use /. + */ +export function unnormalizePath(path: string): string { + return nodePath.normalize(path); +} diff --git a/src/core/utils/public.ts b/src/core/utils/public.ts index 39a9fd85..f78d7fa9 100644 --- a/src/core/utils/public.ts +++ b/src/core/utils/public.ts @@ -1,6 +1,7 @@ import { InternalConfig } from '../types'; import fs from 'fs-extra'; import glob from 'fast-glob'; +import { unnormalizePath } from './paths'; /** * Get all the files in the project's public directory. Returned paths are relative to the @@ -11,5 +12,6 @@ export async function getPublicFiles( ): Promise { if (!(await fs.exists(config.publicDir))) return []; - return await glob('**/*', { cwd: config.publicDir }); + const files = await glob('**/*', { cwd: config.publicDir }); + return files.map(unnormalizePath); } diff --git a/src/core/vite-plugins/multipageMove.ts b/src/core/vite-plugins/multipageMove.ts index dfdb696f..db608369 100644 --- a/src/core/vite-plugins/multipageMove.ts +++ b/src/core/vite-plugins/multipageMove.ts @@ -3,6 +3,7 @@ import { Entrypoint, InternalConfig } from '../types'; import { dirname, extname, resolve } from 'node:path'; import { getEntrypointBundlePath } from '../utils/entrypoints'; import fs, { ensureDir } from 'fs-extra'; +import { normalizePath } from '../utils/paths'; /** * Ensures the HTML files output by a multipage build are in the correct location. This does two @@ -23,11 +24,11 @@ export function multipageMove( name: 'wxt:multipage-move', async writeBundle(_, bundle) { for (const oldBundlePath in bundle) { - // oldBundlePath = 'entrypoints/popup.html' or 'entrypoints/o ptions/index.html' + // oldBundlePath = 'entrypoints/popup.html' or 'entrypoints/options/index.html' // Find a matching entrypoint - oldBundlePath is the same as end end of the input path. const entrypoint = entrypoints.find( - (entry) => !!entry.inputPath.endsWith(oldBundlePath), + (entry) => !!normalizePath(entry.inputPath).endsWith(oldBundlePath), ); if (entrypoint == null) { config.logger.debug('No entrypoint found for', oldBundlePath); diff --git a/src/core/vite-plugins/virtualEntrypoint.ts b/src/core/vite-plugins/virtualEntrypoint.ts index 844189bc..a6308896 100644 --- a/src/core/vite-plugins/virtualEntrypoint.ts +++ b/src/core/vite-plugins/virtualEntrypoint.ts @@ -2,6 +2,7 @@ import { Plugin } from 'vite'; import { Entrypoint, InternalConfig } from '../types'; import fs from 'fs-extra'; import { resolve } from 'path'; +import { normalizePath } from '../utils/paths'; /** * Wraps a user's entrypoint with a vitual version with additional logic. @@ -21,7 +22,7 @@ export function virtualEntrypoin( const index = id.indexOf(virtualId); if (index === -1) return; - const inputPath = id.substring(index + virtualId.length); + const inputPath = normalizePath(id.substring(index + virtualId.length)); return resolvedVirtualId + inputPath; }, async load(id) { diff --git a/src/index.ts b/src/index.ts index d486a8ab..bf80b810 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,10 @@ export async function build(config: InlineConfig): Promise { return await buildInternal(internalConfig); } +/** + * Creates a dev server, pre-builds all the files that need to exist to load the extension, and open + * the browser with the extension installed. + */ export async function createServer( config?: InlineConfig, ): Promise { @@ -54,6 +58,7 @@ export async function createServer( }); server.watcher.on('all', async (event, path, _stats) => { + // Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...) if (path.startsWith(internalConfig.outBaseDir)) return; changeQueue.push([event, path]); diff --git a/tsconfig.base.json b/tsconfig.base.json index d7a7ea30..1dbd78cf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -9,7 +9,7 @@ /* Type Checking */ "strict": true, - "lib": ["DOM", "WebWorker"], + "lib": ["DOM", "WebWorker", "ESNext"], /* Linting */ "noUnusedLocals": true,