├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── dvlp.js ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── scripts └── build.js ├── src ├── _.d.ts ├── _global.d.ts ├── _vendor.d.ts ├── application-host │ ├── _.d.ts │ ├── application-loader.js │ ├── application-worker.js │ └── index.js ├── config.js ├── dvlp-internal.d.ts ├── dvlp-internal.js ├── dvlp-test-browser.d.ts ├── dvlp-test-browser.js ├── dvlp-test.d.ts ├── dvlp-test.js ├── dvlp.d.ts ├── dvlp.js ├── electron-host │ ├── _.d.ts │ ├── electron-entry.js │ ├── electron-worker.js │ ├── index.js │ └── worker-data.js ├── hooks │ ├── _.d.ts │ ├── bundle-dependency.js │ ├── index.js │ └── transform.js ├── mock │ ├── _.d.ts │ ├── index.js │ └── mock-client.js ├── push-events │ ├── _.d.ts │ └── index.js ├── reload │ ├── event-source.js │ ├── reload-client-embed.js │ └── reload-client.js ├── resolver │ ├── _.d.ts │ ├── index.js │ ├── package.js │ └── utils.js ├── server │ ├── _.d.ts │ ├── certificate-validation.js │ ├── handlers.js │ └── index.js ├── test-browser │ └── index.js ├── test-server │ ├── _.d.ts │ └── index.js └── utils │ ├── _.d.ts │ ├── base64Url.js │ ├── bootstrap.js │ ├── broken-named-exports.js │ ├── bundling.js │ ├── expand-path.js │ ├── favicon.js │ ├── file.js │ ├── intercept-client-request.js │ ├── intercept-create-server.js │ ├── intercept-file-access.js │ ├── intercept-in-process.js │ ├── is.js │ ├── log.js │ ├── metrics.js │ ├── mime.js │ ├── module.js │ ├── patch.js │ ├── platform.js │ ├── regexp.js │ ├── request-contexts.js │ ├── request.js │ ├── scripts.js │ ├── send.js │ ├── throttle.js │ ├── url.js │ └── watch.js ├── test ├── browser │ ├── fixtures │ │ └── mock │ │ │ ├── es.json │ │ │ ├── rest.json │ │ │ └── ws.json │ ├── index.html │ └── mock-test.js ├── css │ ├── adopted-doc-styles.css │ ├── adopted-el-styles.css │ ├── global-styles.css │ ├── imported-level2-styles.css │ ├── imported-styles.css │ ├── index.html │ ├── index.js │ └── my-el.js ├── electron │ ├── entry-load-file-with-fetch-mock.js │ ├── entry-load-file.js │ ├── entry-server-worker.js │ ├── entry-server.js │ ├── mock.json │ ├── renderer.html │ ├── renderer.js │ ├── template.js │ └── worker.js ├── integration │ ├── fixtures │ │ ├── app.mjs │ │ ├── assets │ │ │ └── a.js │ │ └── www │ │ │ └── a.css │ ├── server-test.js │ └── test-server-test.js └── unit │ ├── electron-test.js │ ├── file-test.js │ ├── fixtures │ ├── app-api.mjs │ ├── app-create-server.mjs │ ├── app-error.mjs │ ├── app-https.mjs │ ├── app-listener.mjs │ ├── app-multi.mjs │ ├── app-request-error.mjs │ ├── app.mjs │ ├── app.ts │ ├── assets │ │ ├── index.css │ │ └── index.html │ ├── body.mjs │ ├── body.ts │ ├── certificates │ │ ├── dvlp.crt │ │ ├── dvlp.issuer.crt │ │ └── dvlp.key │ ├── component.jsx │ ├── electron-create-server.mjs │ ├── electron-file.mjs │ ├── electron │ │ ├── index.html │ │ └── preload.cjs │ ├── file.esm.js │ ├── file.js │ ├── hooks-bundle.mjs │ ├── hooks-error.mjs │ ├── hooks-request.mjs │ ├── hooks-send.mjs │ ├── hooks-transform-bundle.mjs │ ├── hooks-transform-server.mjs │ ├── hooks-transform.mjs │ ├── mock-push-connect │ │ ├── event-source.json │ │ └── web-socket.json │ ├── mock-push │ │ ├── event-source.json │ │ └── web-socket.json │ ├── mock │ │ ├── 1234.jpg │ │ ├── 1234.json │ │ ├── 4567.mjs │ │ ├── 5678.json │ │ ├── 9012.json │ │ ├── json.json │ │ ├── more │ │ │ └── 3456.json │ │ ├── multi.json │ │ ├── params.json │ │ ├── search.json │ │ └── test.json │ ├── node_modules │ │ ├── .modules.yaml │ │ ├── bar │ │ │ ├── browser.js │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── bat │ │ │ ├── boo │ │ │ │ ├── index.esm.js │ │ │ │ └── package.json │ │ │ ├── browser.js │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── css │ │ │ ├── package.json │ │ │ └── styles.css │ │ └── foo │ │ │ └── foo.js │ ├── package.json │ ├── resolver │ │ ├── baz.js │ │ ├── dir │ │ │ └── index.ts │ │ ├── foo.bar.js │ │ ├── foo.js │ │ ├── linked │ │ │ ├── index.js │ │ │ ├── node_modules │ │ │ │ └── .modules.yaml │ │ │ └── package.json │ │ ├── nested │ │ │ ├── foo.js │ │ │ ├── nested │ │ │ │ └── bar.js │ │ │ ├── node_modules │ │ │ │ └── .modules.yaml │ │ │ └── package.json │ │ ├── node_modules │ │ │ ├── .modules.yaml │ │ │ ├── .pnpm │ │ │ │ └── a@1.0.0 │ │ │ │ │ └── node_modules │ │ │ │ │ └── a │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ ├── @popeindustries │ │ │ │ └── test │ │ │ │ │ ├── lib │ │ │ │ │ └── bar.js │ │ │ │ │ ├── node_modules │ │ │ │ │ ├── foo │ │ │ │ │ │ ├── lib │ │ │ │ │ │ │ ├── bar.js │ │ │ │ │ │ │ └── bat.js │ │ │ │ │ │ └── package.json │ │ │ │ │ └── versioned │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── package.json │ │ │ │ │ ├── package.json │ │ │ │ │ ├── test.css │ │ │ │ │ └── test.js │ │ │ ├── a │ │ │ ├── alias │ │ │ │ ├── index.ts │ │ │ │ └── package.json │ │ │ ├── bar │ │ │ │ ├── index.js │ │ │ │ └── node_modules │ │ │ │ │ └── boo │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ ├── bat │ │ │ │ ├── boo │ │ │ │ │ ├── index.esm.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── package.json │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── browser-hash │ │ │ │ ├── bar.js │ │ │ │ ├── bing.js │ │ │ │ ├── browser │ │ │ │ │ └── foo.js │ │ │ │ ├── foo.js │ │ │ │ ├── node_modules │ │ │ │ │ ├── bar │ │ │ │ │ │ ├── lib │ │ │ │ │ │ │ ├── bar.js │ │ │ │ │ │ │ └── bat.js │ │ │ │ │ │ └── package.json │ │ │ │ │ ├── bat │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── package.json │ │ │ │ │ ├── bing │ │ │ │ │ │ ├── bing.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── package.json │ │ │ │ │ └── foo │ │ │ │ │ │ ├── lib │ │ │ │ │ │ ├── bar.js │ │ │ │ │ │ └── bat.js │ │ │ │ │ │ └── package.json │ │ │ │ ├── package.json │ │ │ │ ├── server │ │ │ │ │ └── foo.js │ │ │ │ └── test.js │ │ │ ├── browser │ │ │ │ ├── browser │ │ │ │ │ └── foo.js │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── exports │ │ │ │ ├── browser.js │ │ │ │ ├── main.js │ │ │ │ ├── nested │ │ │ │ │ ├── index.js │ │ │ │ │ └── only-ts.ts │ │ │ │ ├── package.json │ │ │ │ └── sub │ │ │ │ │ ├── sub-browser-dev.js │ │ │ │ │ ├── sub-browser.js │ │ │ │ │ ├── sub-dev.js │ │ │ │ │ └── sub.js │ │ │ ├── foo-dir │ │ │ │ ├── lib │ │ │ │ │ └── index.js │ │ │ │ └── package.json │ │ │ ├── foo │ │ │ │ ├── index.js │ │ │ │ ├── lib │ │ │ │ │ ├── bar.js │ │ │ │ │ ├── bat.js │ │ │ │ │ └── caseSensitive.js │ │ │ │ ├── node_modules │ │ │ │ │ ├── bar │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── package.json │ │ │ │ │ └── versioned │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── package.json │ │ │ │ └── package.json │ │ │ ├── linked │ │ │ ├── module-browser │ │ │ │ ├── browser.js │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ ├── module │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ └── versioned │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ └── package.json │ ├── route.ts │ ├── script.js │ └── www │ │ ├── $$.js │ │ ├── dep-cjs.js │ │ ├── dep-esm.js │ │ ├── dep.css │ │ ├── dep.ts │ │ ├── dep2.js │ │ ├── error.js │ │ ├── font.woff │ │ ├── index.html │ │ ├── module-with-deps.js │ │ ├── module.js │ │ ├── nested-ts │ │ └── index.ts │ │ ├── nested │ │ ├── foo.jsx │ │ ├── index.html │ │ ├── index.js │ │ └── style.css │ │ ├── otherstyle.css │ │ ├── script.js │ │ ├── spå ces.js │ │ ├── style.css │ │ ├── test.json │ │ └── ts.ts │ ├── hooks-test.js │ ├── index.js │ ├── init.js │ ├── intercept-test.js │ ├── mock-test.js │ ├── patch-test.js │ ├── platform-test.js │ ├── resolver-test.js │ ├── server-test.js │ ├── test-server-test.js │ └── utils.js └── tsconfig.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: dvlp 2 | 3 | on: push 4 | 5 | env: 6 | PNPM_CACHE_FOLDER: .pnpm-store 7 | HUSKY: 0 # Bypass husky commit hook for CI 8 | 9 | jobs: 10 | build_deploy: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest] 14 | node: ['20', '22', '23'] 15 | runs-on: ${{ matrix.os }} 16 | name: Install, build, and test (OS ${{ matrix.os }} - Node ${{ matrix.node }}) 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: 'Install pnpm' 22 | uses: pnpm/action-setup@v3 23 | with: 24 | version: 10.x 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node }} 30 | cache: 'pnpm' 31 | 32 | - name: 'Install dependencies' 33 | run: pnpm --frozen-lockfile --no-optional install 34 | 35 | - name: Build 36 | run: pnpm run build 37 | 38 | - name: Test:unit 39 | timeout-minutes: 1 40 | run: pnpm run test 41 | 42 | - name: Test:integration 43 | run: pnpm run test:integration 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | /node_modules 3 | .DS_Store 4 | .dvlp 5 | /*.js 6 | /*.cjs 7 | /*.d.ts 8 | /*.map 9 | .npmrc 10 | !eslint.config.js -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | *.min.js 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 Alexander Pope 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/dvlp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { dirname, join } from 'node:path'; 4 | import { Command } from 'commander'; 5 | import { fileURLToPath } from 'node:url'; 6 | import { readFileSync } from 'node:fs'; 7 | 8 | const pkg = JSON.parse( 9 | readFileSync( 10 | join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 11 | 'utf8', 12 | ), 13 | ); 14 | const program = new Command(); 15 | 16 | program 17 | .usage('[options] [path...]') 18 | .description( 19 | `Start a development server, restarting and reloading connected clients on file changes. 20 | Serves static files from one or more "path" directories, or a custom application 21 | server if "path" is a single application server file.`, 22 | ) 23 | .option('-p, --port ', 'port number', parseInt) 24 | .option( 25 | '-m, --mock ', 26 | 'path to mock files (directory, file, glob pattern)', 27 | ) 28 | .option('-k, --hooks ', 'path to optional hooks registration file') 29 | .option('-e, --electron', 'run "path" file as electron.js entry file') 30 | .option( 31 | '--ssl ', 32 | `enable https mode by specifying path to directory containing ".crt" and ".key" files (directory, glob pattern)`, 33 | ) 34 | .option('-s, --silent', 'suppress all logging') 35 | .option('--verbose', 'enable verbose logging') 36 | .option('--no-reload', 'disable reloading connected clients on file change') 37 | .version(pkg.version, '-v, --version', 'output the current version') 38 | .arguments('[path...]') 39 | .action(boot); 40 | 41 | program.allowUnknownOption(true).parse(process.argv); 42 | 43 | async function boot(path = [process.cwd()]) { 44 | try { 45 | const { server } = await import('dvlp'); 46 | const options = program.opts(); 47 | 48 | await server( 49 | path.filter((arg) => !arg.startsWith('-')), 50 | { 51 | argv: program.args.filter((arg) => arg.startsWith('-')), 52 | certsPath: options.ssl, 53 | electron: options.electron, 54 | hooksPath: options.hooks, 55 | mockPath: options.mock, 56 | port: options.port, 57 | reload: options.reload, 58 | silent: options.silent, 59 | verbose: options.verbose, 60 | }, 61 | ); 62 | } catch (err) { 63 | console.error(err); 64 | process.exit(1); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import globals from 'globals'; 3 | import prettier from 'eslint-config-prettier'; 4 | import tsEslint from 'typescript-eslint'; 5 | 6 | export default tsEslint.config( 7 | eslint.configs.recommended, 8 | ...tsEslint.configs.recommended, 9 | prettier, 10 | { 11 | languageOptions: { 12 | ecmaVersion: 2024, 13 | globals: { 14 | ...globals.browser, 15 | ...globals.mocha, 16 | ...globals.node, 17 | globalThis: true, 18 | URLPattern: true, 19 | }, 20 | sourceType: 'module', 21 | }, 22 | rules: { 23 | '@typescript-eslint/ban-ts-comment': [ 24 | 'error', 25 | { 26 | 'ts-expect-error': 'allow-with-description', 27 | 'ts-nocheck': 'allow-with-description', 28 | }, 29 | ], 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/no-unused-expressions': 'off', 32 | '@typescript-eslint/no-unused-vars': [ 33 | 'warn', 34 | { 35 | args: 'none', 36 | argsIgnorePattern: '^_', 37 | ignoreRestSiblings: true, 38 | vars: 'all', 39 | }, 40 | ], 41 | 'prefer-const': ['error', { destructuring: 'all' }], 42 | 'sort-imports': [ 43 | 'warn', 44 | { 45 | ignoreCase: true, 46 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 47 | }, 48 | ], 49 | }, 50 | }, 51 | // NOTE: this needs to be a separate object to trigger "global" ignore 52 | { 53 | ignores: ['**/.*', '**/test/**/fixtures'], 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dvlp", 3 | "version": "17.0.0", 4 | "description": "A no-nonsense dev server toolkit to help you develop quickly and easily for the web", 5 | "type": "module", 6 | "types": "dvlp.d.ts", 7 | "repository": "https://github.com/popeindustries/dvlp.git", 8 | "author": "Alexander Pope ", 9 | "license": "MIT", 10 | "bin": { 11 | "dvlp": "bin/dvlp.js" 12 | }, 13 | "exports": { 14 | ".": "./src/dvlp.js", 15 | "./internal": "./src/dvlp-internal.js", 16 | "./dvlp-internal.js": "./src/dvlp-internal.js", 17 | "./test-browser": "./src/dvlp-test-browser.js", 18 | "./dvlp-test-browser.js": "./src/dvlp-test-browser.js", 19 | "./test": "./src/dvlp-test.js", 20 | "./dvlp-test.js": "./src/dvlp-test.js" 21 | }, 22 | "publishConfig": { 23 | "exports": { 24 | ".": "./dvlp.js", 25 | "./internal": "./dvlp-internal.js", 26 | "./dvlp-internal.js": "./dvlp-internal.js", 27 | "./test-browser": "./dvlp-test-browser.js", 28 | "./dvlp-test-browser.js": "./dvlp-test-browser.js", 29 | "./test": "./dvlp-test.js", 30 | "./dvlp-test.js": "./dvlp-test.js" 31 | } 32 | }, 33 | "dependencies": { 34 | "commander": "^13.1.0", 35 | "esbuild": "^0.25.4" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.26.0", 39 | "@fidm/x509": "^1.2.1", 40 | "@types/debug": "^4.1.12", 41 | "@types/glob": "^8.1.0", 42 | "@types/mocha": "^10.0.10", 43 | "@types/node": "^22.15.11", 44 | "@types/platform": "^1.3.6", 45 | "@types/send": "^0.17.4", 46 | "chai": "^5.2.0", 47 | "chalk": "^5.4.1", 48 | "chokidar": "^4.0.3", 49 | "cjs-module-lexer": "^2.1.0", 50 | "cross-env": "^7.0.3", 51 | "debug": "^4.4.0", 52 | "electron": "36.1.0", 53 | "es-module-lexer": "^1.7.0", 54 | "eslint": "^9.26.0", 55 | "eslint-config-prettier": "^10.1.2", 56 | "eventsource": "^3.0.6", 57 | "fast-glob": "^3.3.3", 58 | "fastify": "^5.3.2", 59 | "faye-websocket": "^0.11.4", 60 | "globals": "^16.0.0", 61 | "husky": "^9.1.7", 62 | "lint-staged": "^15.5.1", 63 | "lit-html": "^3.3.0", 64 | "mocha": "^11.2.2", 65 | "path-to-regexp": "^8.2.0", 66 | "permessage-deflate": "^0.1.7", 67 | "platform": "^1.3.6", 68 | "prettier": "^3.5.3", 69 | "react": "^18.3.1", 70 | "resolve.exports": "^2.0.3", 71 | "semver": "^7.7.0", 72 | "terser": "^5.39.0", 73 | "typescript": "^5.8.3", 74 | "typescript-eslint": "^8.32.0" 75 | }, 76 | "engines": { 77 | "node": ">=20", 78 | "pnpm": ">=10" 79 | }, 80 | "pnpm": { 81 | "overrides": { 82 | "debug": "^4.4.0" 83 | }, 84 | "onlyBuiltDependencies": [ 85 | "electron", 86 | "esbuild" 87 | ] 88 | }, 89 | "scripts": { 90 | "build": "node ./scripts/build.js", 91 | "clean": "git clean -x -f", 92 | "format": "prettier --write './{src,test}/**/*.{js,json}'", 93 | "lint": "pnpm run lint:src && pnpm run lint:types", 94 | "lint:src": "eslint './{src,test}/**/*.js'", 95 | "lint:types": "tsc --noEmit --skipLibCheck ", 96 | "prepublishOnly": "pnpm run build", 97 | "test": "cross-env NODE_ENV=dvlptest NODE_TLS_REJECT_UNAUTHORIZED='0' mocha --reporter spec --bail --exit --timeout 10000 --require ./test/unit/init.js test/unit/index.js", 98 | "test:all": "pnpm run test && pnpm run test:integration", 99 | "test:browser": "pnpm run build && ./bin/dvlp.js --mock test/browser/fixtures/mock test/browser", 100 | "test:integration": "pnpm run build && cross-env NODE_ENV=dvlptest mocha test/integration/*-test.js --reporter spec --exit --timeout 10000", 101 | "prepare": "husky" 102 | }, 103 | "prettier": { 104 | "arrowParens": "always", 105 | "printWidth": 80, 106 | "singleQuote": true, 107 | "trailingComma": "all" 108 | }, 109 | "lint-staged": { 110 | "*.js": [ 111 | "prettier --write", 112 | "eslint" 113 | ] 114 | }, 115 | "files": [ 116 | "bin", 117 | "*.d.ts", 118 | "*.js", 119 | "*.cjs", 120 | "README.MD" 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import fs from 'fs'; 3 | import glob from 'fast-glob'; 4 | import { minify } from 'terser'; 5 | import path from 'path'; 6 | 7 | const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); 8 | 9 | const reloadClient = ( 10 | await minify(fs.readFileSync('src/reload/reload-client.js', 'utf8')) 11 | ).code.replace(/(["\\])/g, '\\$1'); 12 | const mockClient = ( 13 | await minify(fs.readFileSync('src/mock/mock-client.js', 'utf8'), { 14 | // Preserve 'cache' var for regex replacement 15 | mangle: { reserved: ['cache'] }, 16 | }) 17 | ).code.replace(/(["\\])/g, '\\$1'); 18 | const banner = { 19 | js: "import { createRequire as createRequireBecauseEsbuild } from 'module'; \nconst require = createRequireBecauseEsbuild(import.meta.url);", 20 | }; 21 | const define = { 22 | 'global.$RELOAD_CLIENT': `'${reloadClient}'`, 23 | 'global.$MOCK_CLIENT': `"${mockClient}"`, 24 | 'global.$VERSION': `'${pkg.version}'`, 25 | }; 26 | const external = ['electron', 'esbuild', 'fsevents', 'dvlp/internal']; 27 | let types = ''; 28 | 29 | for (const typePath of glob.sync('src/**/_.d.ts')) { 30 | types += `// ${typePath}\n${fs.readFileSync( 31 | path.resolve(typePath), 32 | 'utf-8', 33 | )}\n`; 34 | } 35 | 36 | types = types.replace( 37 | /(declare) (interface|type|enum|namespace|function|class)/g, 38 | 'export $2', 39 | ); 40 | 41 | fs.writeFileSync( 42 | 'dvlp.d.ts', 43 | `${fs.readFileSync('src/dvlp.d.ts', 'utf-8')}\n${types}`, 44 | 'utf8', 45 | ); 46 | fs.copyFileSync('src/dvlp-test.d.ts', 'dvlp-test.d.ts'); 47 | fs.copyFileSync('src/dvlp-test-browser.d.ts', 'dvlp-test-browser.d.ts'); 48 | 49 | await esbuild.build({ 50 | bundle: true, 51 | entryPoints: ['./src/dvlp-test-browser.js'], 52 | format: 'esm', 53 | outfile: 'dvlp-test-browser.js', 54 | target: 'es2020', 55 | }); 56 | 57 | await esbuild.build({ 58 | banner, 59 | bundle: true, 60 | define, 61 | entryPoints: ['./src/dvlp-test.js'], 62 | format: 'esm', 63 | outfile: 'dvlp-test.js', 64 | platform: 'node', 65 | target: 'node18', 66 | }); 67 | 68 | await esbuild.build({ 69 | banner, 70 | bundle: true, 71 | define, 72 | entryNames: '[name]', 73 | entryPoints: [ 74 | './src/dvlp.js', 75 | './src/dvlp-internal.js', 76 | './src/application-host/application-worker.js', 77 | './src/electron-host/electron-worker.js', 78 | ], 79 | external, 80 | format: 'esm', 81 | outdir: '.', 82 | platform: 'node', 83 | splitting: false, 84 | target: 'node18', 85 | }); 86 | 87 | await esbuild.build({ 88 | bundle: true, 89 | entryNames: '[name]', 90 | entryPoints: ['./src/application-host/application-loader.js'], 91 | external, 92 | format: 'esm', 93 | splitting: false, 94 | target: 'node18', 95 | outdir: '.', 96 | platform: 'node', 97 | plugins: [ 98 | // Replace `log.js` with dummy 99 | { 100 | name: 'dummylog', 101 | setup(build) { 102 | build.onLoad({ filter: /utils\/log.js$/ }, (args) => { 103 | return { 104 | contents: ` 105 | export function error() {}; 106 | export function noisyWarn() {}; 107 | export function warn() {}; 108 | export const WARN_MISSING_EXTENSION = ''; 109 | export const WARN_PACKAGE_INDEX = ''; 110 | `, 111 | loader: 'js', 112 | }; 113 | }); 114 | }, 115 | }, 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /src/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Config { 2 | activePort: number; 3 | applicationLoaderURL: import('url').URL; 4 | brokenNamedExportsPackages: Record>; 5 | bundleDirMetaPath: string; 6 | bundleDirName: string; 7 | bundleDirPath: string; 8 | cacheDirPath: string; 9 | defaultPort: number; 10 | directories: Array; 11 | dirPath: string; 12 | dvlpDirPath: string; 13 | electronEntryURL: import('url').URL; 14 | esbuildTargetByExtension: { 15 | [extension: string]: string; 16 | }; 17 | extensionsByType: { 18 | [type: string]: Array; 19 | }; 20 | latency: number; 21 | maxAge: string; 22 | maxAgeLong: string; 23 | serverStartTimeout: number; 24 | testing: boolean; 25 | typesByExtension: { 26 | [extension: string]: ContentType; 27 | }; 28 | version: string; 29 | versionDirPath: string; 30 | } 31 | 32 | declare type ContentType = 'css' | 'html' | 'js'; 33 | 34 | declare interface Entry { 35 | directories: Array; 36 | isApp: boolean; 37 | isElectron: boolean; 38 | isSecure: boolean; 39 | isStatic: boolean; 40 | main: string | undefined; 41 | } 42 | 43 | type Http2ServerRequest = import('http2').Http2ServerRequest; 44 | type Http2ServerResponse = import('http2').Http2ServerResponse; 45 | type IncomingMessage = import('http').IncomingMessage; 46 | type ServerResponse = import('http').ServerResponse; 47 | type HttpServer = import('http').Server; 48 | type HttpServerOptions = import('http').ServerOptions; 49 | type Http2SecureServer = import('http2').Http2SecureServer; 50 | type Http2SecureServerOptions = import('http2').SecureServerOptions; 51 | type esbuild = { 52 | build( 53 | options: import('esbuild').BuildOptions & { write: false }, 54 | ): Promise; 55 | transform( 56 | input: string, 57 | options?: import('esbuild').TransformOptions, 58 | ): Promise; 59 | }; 60 | 61 | type Req = (IncomingMessage | Http2ServerRequest) & { 62 | filePath: string; 63 | type?: ContentType; 64 | url: string; 65 | params?: Record; 66 | }; 67 | type Res = (ServerResponse | Http2ServerResponse) & { 68 | bundled: boolean; 69 | encoding: string; 70 | metrics: Metrics; 71 | mocked: boolean; 72 | rerouted: boolean; 73 | transformed: boolean; 74 | unhandled: boolean; 75 | url: string; 76 | error?: Error; 77 | }; 78 | type RequestHandler = (req: Req, res: Res) => void; 79 | interface DestroyableHttpServer extends HttpServer { 80 | destroy(): void; 81 | } 82 | -------------------------------------------------------------------------------- /src/_global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Global { 3 | $MOCK_CLIENT?: string; 4 | $RELOAD_CLIENT?: string; 5 | $VERSION: string; 6 | sources: Set; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/_vendor.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'permessage-deflate' { 2 | export function deflate(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/application-host/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ApplicationWorker { 2 | readonly activeProcess?: import('node:worker_threads').Worker; 3 | readonly origins: Set; 4 | readonly isListening: boolean; 5 | /** 6 | * Add `filePaths` to file watcher 7 | */ 8 | addWatchFiles(filePaths: string | Array): void; 9 | /** 10 | * Send message to the application thread 11 | */ 12 | sendMessage(message: string | object | number | boolean | bigint): void; 13 | } 14 | 15 | declare interface ApplicationProcessWorkerData { 16 | hostOrigin: string; 17 | postMessage(msg: ApplicationWorkerMessage): void; 18 | main?: string; 19 | serializedMocks?: Array; 20 | } 21 | 22 | declare type ApplicationHostMessage = { type: 'start'; main: string }; 23 | declare type ApplicationLoaderMessage = { 24 | type: 'dependency'; 25 | filePath: string; 26 | }; 27 | declare type ApplicationWorkerMessage = 28 | | { type: 'error'; error: string } 29 | | { type: 'listening'; origin: string } 30 | | { type: 'started' } 31 | | { type: 'watch'; filePath: string; mode: 'read' | 'write' }; 32 | 33 | declare interface ApplicationWorkerPendingHandle { 34 | promise: Promise<{ body: string; href: string }>; 35 | resolve: (value: { body: string; href: string }) => void; 36 | reject: (value?: unknown) => void; 37 | } 38 | -------------------------------------------------------------------------------- /src/application-host/application-loader.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck - this file is not type checked 2 | /* global customHooks */ 3 | 4 | import esbuild from 'esbuild'; 5 | import { fileURLToPath } from 'node:url'; 6 | import fs from 'node:fs'; 7 | import { nodeResolve } from 'dvlp/internal'; 8 | 9 | const IS_WIN32 = process.platform === 'win32'; 10 | const RE_EXTS = /\.(tsx?|json)$/; 11 | 12 | let port; 13 | 14 | export function initialize(data = {}) { 15 | port = data.port; 16 | } 17 | 18 | export function resolve(specifier, context, nextResolve) { 19 | if (customHooks.onServerResolve !== undefined) { 20 | const resolved = customHooks.onServerResolve( 21 | specifier, 22 | context, 23 | (specifier, context) => doResolve(specifier, context, nextResolve), 24 | ); 25 | resolved.shortCircuit = true; 26 | return resolved; 27 | } 28 | 29 | return doResolve(specifier, context, nextResolve); 30 | } 31 | 32 | function doResolve(specifier, context, nextResolve) { 33 | if (!specifier.startsWith('node:')) { 34 | const resolved = nodeResolve( 35 | specifier, 36 | context.parentURL ? fileURLToPath(context.parentURL) : undefined, 37 | ); 38 | if (resolved !== undefined) { 39 | resolved.shortCircuit = true; 40 | return resolved; 41 | } 42 | } 43 | 44 | return nextResolve(specifier, context); 45 | } 46 | 47 | export function load(url, context, nextLoad) { 48 | port?.postMessage({ 49 | type: 'dependency', 50 | filePath: url.startsWith('file://') ? fileURLToPath(url) : url, 51 | }); 52 | 53 | if (customHooks.onServerTransform !== undefined) { 54 | const result = customHooks.onServerTransform(url, context, (url, context) => 55 | doLoad(url, context, nextLoad), 56 | ); 57 | result.shortCircuit = true; 58 | return result; 59 | } 60 | 61 | return doLoad(url, context, nextLoad); 62 | } 63 | 64 | function doLoad(url, context, nextLoad) { 65 | if (RE_EXTS.test(new URL(url).pathname)) { 66 | const { format } = context; 67 | 68 | const filename = IS_WIN32 ? url : fileURLToPath(url); 69 | const source = fs.readFileSync(new URL(url), { encoding: 'utf8' }); 70 | const { code } = transform(source, filename, url, format); 71 | 72 | return { format: 'module', source: code, shortCircuit: true }; 73 | } 74 | 75 | return nextLoad(url, context); 76 | } 77 | 78 | function transform(source, filename, url, format) { 79 | const { code, warnings } = esbuild.transformSync(source.toString(), { 80 | sourcefile: filename, 81 | sourcemap: 'inline', 82 | loader: new URL(url).pathname.match(RE_EXTS)[1], 83 | target: 'node' + process.versions.node, 84 | format: 'esm', 85 | }); 86 | 87 | if (warnings && warnings.length > 0) { 88 | for (const warning of warnings) { 89 | console.warn(warning.location); 90 | console.warn(warning.text); 91 | } 92 | } 93 | 94 | return { code }; 95 | } 96 | -------------------------------------------------------------------------------- /src/application-host/application-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { import('worker_threads').MessagePort } MessagePort 3 | */ 4 | 5 | import { config, error, interceptInProcess } from 'dvlp/internal'; 6 | import { MessageChannel } from 'node:worker_threads'; 7 | import module from 'node:module'; 8 | import { workerData } from 'node:worker_threads'; 9 | 10 | const messagePort = /** @type { MessagePort } */ (workerData.messagePort); 11 | 12 | interceptInProcess({ 13 | hostOrigin: workerData.hostOrigin, 14 | postMessage: /** @param { ApplicationWorkerMessage } msg */ (msg) => { 15 | try { 16 | messagePort.postMessage(msg); 17 | } catch { 18 | // Ignroe 19 | } 20 | }, 21 | serializedMocks: workerData.serializedMocks, 22 | }); 23 | 24 | messagePort.on( 25 | 'message', 26 | /** @param { ApplicationHostMessage } msg */ 27 | async (msg) => { 28 | if (msg.type === 'start') { 29 | try { 30 | await import(msg.main); 31 | messagePort.postMessage({ type: 'started' }); 32 | } catch (err) { 33 | messagePort.postMessage({ type: 'error', error: err }); 34 | } finally { 35 | // TODO: deprecate with Node18 36 | if ('sources' in global) { 37 | for (const filePath of /** @type { Set } */ ( 38 | global.sources 39 | )) { 40 | messagePort.postMessage({ type: 'watch', filePath, mode: 'read' }); 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | ); 47 | 48 | process.on('uncaughtException', error); 49 | process.on('unhandledRejection', error); 50 | 51 | if ('register' in module) { 52 | /** 53 | * @type { { parentURL: string, data?: unknown, transferList?: Array } } 54 | */ 55 | const options = { 56 | parentURL: import.meta.url, 57 | }; 58 | 59 | // Disable in CI to prevent process from hanging due to port transfer(?) 60 | if (!process.env.CI) { 61 | const { port1, port2 } = new MessageChannel(); 62 | 63 | port1.unref(); 64 | port1.on( 65 | 'message', 66 | /** @param { ApplicationLoaderMessage } msg */ 67 | (msg) => { 68 | if (msg.type === 'dependency') { 69 | const { filePath } = msg; 70 | 71 | messagePort.postMessage({ type: 'watch', filePath, mode: 'read' }); 72 | } 73 | }, 74 | ); 75 | 76 | options.data = { port: port2 }; 77 | options.transferList = [port2]; 78 | } 79 | 80 | module.register(config.applicationLoaderURL.href, options); 81 | } 82 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import brokenNamedExportsPackages from './utils/broken-named-exports.js'; 2 | import path from 'node:path'; 3 | import { pathToFileURL } from 'node:url'; 4 | 5 | const DIR_NAME = '.dvlp'; 6 | const TESTING = 7 | process.env.NODE_ENV === 'dvlptest' || process.env.CI != undefined; 8 | // @ts-expect-error - Replaced during build 9 | const VERSION = global.$VERSION || '0.0.0'; 10 | 11 | const dirPath = path.resolve(DIR_NAME); 12 | const versionDirPath = path.join(dirPath, VERSION); 13 | const applicationLoaderURL = pathToFileURL( 14 | path.join(versionDirPath, 'app-loader.mjs'), 15 | ); 16 | const bundleDirName = path.join(DIR_NAME, VERSION, 'bundled'); 17 | const bundleDirPath = path.resolve(bundleDirName); 18 | const bundleDirMetaPath = path.join(bundleDirPath, '__meta__.json'); 19 | const cacheDirPath = path.join(versionDirPath, 'cached'); 20 | const defaultPort = process.env.PORT ? Number(process.env.PORT) : 8080; 21 | const electronEntryURL = pathToFileURL( 22 | path.join(versionDirPath, 'electron-entry.mjs'), 23 | ); 24 | 25 | /** 26 | * @type { Config } 27 | */ 28 | const config = { 29 | activePort: defaultPort, 30 | applicationLoaderURL, 31 | brokenNamedExportsPackages, 32 | bundleDirPath, 33 | bundleDirMetaPath, 34 | bundleDirName, 35 | cacheDirPath, 36 | defaultPort, 37 | directories: [], 38 | dirPath, 39 | dvlpDirPath: path.resolve(DIR_NAME), 40 | electronEntryURL, 41 | esbuildTargetByExtension: { 42 | '.js': 'js', 43 | '.mjs': 'js', 44 | '.cjs': 'js', 45 | '.json': 'json', 46 | '.jsx': 'jsx', 47 | '.ts': 'ts', 48 | '.tsx': 'tsx', 49 | '.mts': 'ts', 50 | '.cts': 'ts', 51 | }, 52 | // Ordered to trigger transpiling if necessary 53 | extensionsByType: { 54 | css: ['.pcss', '.sass', '.scss', '.less', '.styl', '.stylus', '.css'], 55 | html: [ 56 | '.nunjs', 57 | '.nunjucks', 58 | '.hbs', 59 | '.handlebars', 60 | '.dust', 61 | '.html', 62 | '.htm', 63 | ], 64 | js: ['.ts', '.mts', '.cts', '.tsx', '.jsx', '.mjs', '.cjs', '.js', '.json'], 65 | }, 66 | latency: 50, 67 | maxAge: '60', 68 | maxAgeLong: '3600', 69 | serverStartTimeout: TESTING ? 4000 : 10000, 70 | testing: TESTING, 71 | typesByExtension: { 72 | '.css': 'css', 73 | '.pcss': 'css', 74 | '.sass': 'css', 75 | '.scss': 'css', 76 | '.less': 'css', 77 | '.styl': 'css', 78 | '.stylus': 'css', 79 | '.html': 'html', 80 | '.htm': 'html', 81 | '.nunjs': 'html', 82 | '.nunjucks': 'html', 83 | '.hbs': 'html', 84 | '.handlebars': 'html', 85 | '.dust': 'html', 86 | '.js': 'js', 87 | '.mjs': 'js', 88 | '.cjs': 'js', 89 | '.json': 'js', 90 | '.jsx': 'js', 91 | '.ts': 'js', 92 | '.tsx': 'js', 93 | '.mts': 'js', 94 | '.cts': 'js', 95 | }, 96 | version: VERSION, 97 | versionDirPath, 98 | }; 99 | 100 | export default config; 101 | -------------------------------------------------------------------------------- /src/dvlp-internal.d.ts: -------------------------------------------------------------------------------- 1 | export function info(msg: string): void; 2 | export function noisyInfo(msg: string): void; 3 | export function warn(...args: Array): void; 4 | export function error(...args: Array): void; 5 | export function fatal(...args: Array): void; 6 | export function bootstrapElectron(): Promise; 7 | export function filePathToUrlPathname(filePath: string): string; 8 | export function getDependencies( 9 | filePath: string, 10 | platform: 'browser' | 'node', 11 | ): Set; 12 | export function getElectronWorkerData(): ElectronProcessWorkerData; 13 | export function interceptClientRequest(fn: (url: URL) => boolean): () => void; 14 | export function interceptCreateServer( 15 | reservedPort: number, 16 | fn: (port: number) => void, 17 | ): () => void; 18 | export function interceptInProcess( 19 | workerData: ApplicationProcessWorkerData | ElectronProcessWorkerData, 20 | ): void; 21 | export function isEqualSearchParams( 22 | params1: URLSearchParams, 23 | params2: URLSearchParams, 24 | ): boolean; 25 | export { default as config } from './config.js'; 26 | -------------------------------------------------------------------------------- /src/dvlp-internal.js: -------------------------------------------------------------------------------- 1 | export { info, noisyInfo, warn, error, fatal } from './utils/log.js'; 2 | export { bootstrapElectron } from './electron-host/electron-entry.js'; 3 | export { filePathToUrlPathname } from './utils/url.js'; 4 | export { getDependencies } from './utils/module.js'; 5 | export { getElectronWorkerData } from './electron-host/worker-data.js'; 6 | export { interceptInProcess } from './utils/intercept-in-process.js'; 7 | export { nodeResolve } from './resolver/index.js'; 8 | export { default as config } from './config.js'; 9 | -------------------------------------------------------------------------------- /src/dvlp-test-browser.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MockPushEvent, 3 | MockPushStream, 4 | MockRequest, 5 | MockResponse, 6 | MockResponseHandler, 7 | PushEvent, 8 | } from './dvlp.js'; 9 | 10 | export { 11 | MockPushEvent, 12 | MockPushStream, 13 | MockRequest, 14 | MockResponse, 15 | MockResponseHandler, 16 | PushEvent, 17 | }; 18 | 19 | export namespace testBrowser { 20 | /** 21 | * Disable all external network connections, 22 | * and optionally reroute all external requests to this server with `rerouteAllRequests=true` 23 | */ 24 | function disableNetwork(rerouteAllRequests?: boolean): void; 25 | /** 26 | * Re-enable all external network connections 27 | */ 28 | function enableNetwork(): void; 29 | /** 30 | * Add mock response for "req" 31 | */ 32 | function mockResponse( 33 | req: string | MockRequest, 34 | res?: MockResponse | MockResponseHandler, 35 | once?: boolean, 36 | onMockCallback?: () => void, 37 | ): () => void; 38 | /** 39 | * Register mock push "events" for "stream" 40 | */ 41 | function mockPushEvents( 42 | stream: string | MockPushStream, 43 | events: MockPushEvent | Array, 44 | onSendCallback?: (data: any) => void, 45 | ): () => void; 46 | /** 47 | * Push data to WebSocket/EventSource clients 48 | * A string passed as `event` will be handled as a named mock push event 49 | */ 50 | function pushEvent(stream: string, event?: string | PushEvent): void; 51 | } 52 | 53 | declare global { 54 | interface Window { 55 | dvlp: typeof testBrowser; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/dvlp-test-browser.js: -------------------------------------------------------------------------------- 1 | export { testBrowser } from './test-browser/index.js'; 2 | -------------------------------------------------------------------------------- /src/dvlp-test.d.ts: -------------------------------------------------------------------------------- 1 | import { Req, Res, TestServer, TestServerOptions } from './dvlp.js'; 2 | 3 | export { Req, Res, TestServer, TestServerOptions }; 4 | 5 | /** 6 | * Factory for creating `TestServer` instances 7 | */ 8 | export function testServer(options: TestServerOptions): Promise; 9 | 10 | export namespace testServer { 11 | /** 12 | * Disable all external network connections, 13 | * and optionally reroute all external requests to this server with `rerouteAllRequests=true` 14 | */ 15 | function disableNetwork(rerouteAllRequests?: boolean): void; 16 | /** 17 | * Re-enable all external network connections 18 | */ 19 | function enableNetwork(): void; 20 | /** 21 | * Default mock response handler for network hang 22 | */ 23 | function mockHangResponseHandler(url: URL, req: Req, res: Res): undefined; 24 | /** 25 | * Default mock response handler for 500 response 26 | */ 27 | function mockErrorResponseHandler(url: URL, req: Req, res: Res): undefined; 28 | /** 29 | * Default mock response handler for 404 response 30 | */ 31 | function mockMissingResponseHandler(url: URL, req: Req, res: Res): undefined; 32 | /** 33 | * Default mock response handler for offline 34 | */ 35 | function mockOfflineResponseHandler(url: URL, req: Req, res: Res): undefined; 36 | } 37 | -------------------------------------------------------------------------------- /src/dvlp-test.js: -------------------------------------------------------------------------------- 1 | import config from './config.js'; 2 | import { interceptClientRequest } from './utils/intercept-client-request.js'; 3 | import { isLocalhost } from './utils/is.js'; 4 | import log from './utils/log.js'; 5 | import { TestServer } from './test-server/index.js'; 6 | 7 | /** @type { Set } */ 8 | const instances = new Set(); 9 | let reroute = false; 10 | let networkDisabled = false; 11 | /** @type { () => void | undefined } */ 12 | let uninterceptClientRequest; 13 | 14 | /** 15 | * Create test server 16 | * 17 | * @param { TestServerOptions } [options] 18 | * @returns { Promise } 19 | */ 20 | export async function testServer(options) { 21 | enableRequestIntercept(); 22 | 23 | const server = new TestServer(options || {}); 24 | 25 | // @ts-expect-error: private 26 | await server._start(); 27 | 28 | // Force silent mode to suppress logging 29 | log.silent = true; 30 | 31 | instances.add(server); 32 | 33 | const originalDestroy = server.destroy; 34 | 35 | server.destroy = function destroy() { 36 | instances.delete(server); 37 | return originalDestroy.call(server); 38 | }; 39 | 40 | return server; 41 | } 42 | 43 | /** 44 | * Disable all external network connections 45 | * and optionally reroute all external requests to this server 46 | * 47 | * @param { boolean } [rerouteAllRequests] 48 | * @returns { void } 49 | */ 50 | testServer.disableNetwork = function disableNetwork( 51 | rerouteAllRequests = false, 52 | ) { 53 | enableRequestIntercept(); 54 | networkDisabled = true; 55 | reroute = rerouteAllRequests; 56 | }; 57 | 58 | /** 59 | * Re-enable all external network connections 60 | * 61 | * @returns { void } 62 | */ 63 | testServer.enableNetwork = function enableNetwork() { 64 | uninterceptClientRequest?.(); 65 | networkDisabled = false; 66 | reroute = false; 67 | }; 68 | 69 | /** 70 | * Default mock response handler for network hang 71 | * 72 | * @param { Req } req 73 | * @param { Res } res 74 | * @returns { undefined } 75 | */ 76 | testServer.mockHangResponseHandler = function mockHangResponseHandler( 77 | req, 78 | res, 79 | ) { 80 | return; 81 | }; 82 | 83 | /** 84 | * Default mock response handler for 500 response 85 | * 86 | * @param { Req } req 87 | * @param { Res } res 88 | * @returns { undefined } 89 | */ 90 | testServer.mockErrorResponseHandler = function mockErrorResponseHandler( 91 | req, 92 | res, 93 | ) { 94 | res.writeHead(500); 95 | res.error = Error('error'); 96 | res.end('error'); 97 | return; 98 | }; 99 | 100 | /** 101 | * Default mock response handler for 404 response 102 | * 103 | * @param { Req } req 104 | * @param { Res } res 105 | * @returns { undefined } 106 | */ 107 | testServer.mockMissingResponseHandler = function mockMissingResponseHandler( 108 | req, 109 | res, 110 | ) { 111 | res.writeHead(404); 112 | res.end('missing'); 113 | return; 114 | }; 115 | 116 | /** 117 | * Default mock response handler for offline 118 | * 119 | * @param { Req } req 120 | * @param { Res } res 121 | * @returns { undefined } 122 | */ 123 | testServer.mockOfflineResponseHandler = function mockOfflineResponseHandler( 124 | req, 125 | res, 126 | ) { 127 | req.socket.destroy(); 128 | return; 129 | }; 130 | 131 | /** 132 | * Enable request interception to allow mocking/network disabling 133 | */ 134 | function enableRequestIntercept() { 135 | if (uninterceptClientRequest === undefined) { 136 | uninterceptClientRequest = interceptClientRequest((url) => { 137 | const isMocked = Array.from(instances).some((instance) => { 138 | return instance.mocks.hasMatch(url); 139 | }); 140 | const hostname = url.hostname || url.host; 141 | 142 | // Allow mocked requests to pass-through and be intercepted by mock/index.js 143 | if (!isMocked && !isLocalhost(hostname)) { 144 | if (reroute) { 145 | // Reroute back to this server 146 | url.protocol = 'http:'; 147 | url.host = url.hostname = `localhost:${config.activePort}`; 148 | return true; 149 | } else if (networkDisabled) { 150 | throw Error(`network connections disabled. Unable to request ${url}`); 151 | } 152 | } 153 | 154 | return false; 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/dvlp.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Retrieve all dependencies for "filePath" 5 | * 6 | * @param { string } filePath 7 | * @param { 'browser' | 'node' } platform 8 | */ 9 | export function getDependencies( 10 | filePath: string, 11 | platform: 'browser' | 'node', 12 | ): Promise>; 13 | 14 | /** 15 | * Factory for creating `Server` instances 16 | */ 17 | export function server( 18 | filePath?: string | Array, 19 | options?: ServerOptions, 20 | ): Promise; 21 | -------------------------------------------------------------------------------- /src/electron-host/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ElectronProcess { 2 | readonly activeThread?: import('node:child_process').ChildProcess; 3 | readonly origins: Set; 4 | readonly isListening: boolean; 5 | /** 6 | * Add `filePaths` to file watcher 7 | */ 8 | addWatchFiles(filePaths: string | Array): void; 9 | /** 10 | * Send message to the electron process 11 | */ 12 | sendMessage(message: string | object | number | boolean | bigint): void; 13 | } 14 | 15 | declare interface ElectronProcessWorkerData { 16 | hostOrigin: string; 17 | main: string; 18 | postMessage(msg: ElectronProcessMessage): void; 19 | serializedMocks?: Array; 20 | } 21 | 22 | declare type ElectronProcessMessage = 23 | | { type: 'started' } 24 | | { 25 | type: 'listening'; 26 | origin: string; 27 | } 28 | | { type: 'watch'; filePath: string; mode: 'read' | 'write' }; 29 | -------------------------------------------------------------------------------- /src/electron-host/electron-worker.js: -------------------------------------------------------------------------------- 1 | import { interceptInProcess } from 'dvlp/internal'; 2 | import { workerData } from 'node:worker_threads'; 3 | 4 | const messagePort = /** @type { import('worker_threads').MessagePort } */ ( 5 | workerData.dvlp.messagePort 6 | ); 7 | 8 | interceptInProcess({ 9 | hostOrigin: workerData.dvlp.hostOrigin, 10 | postMessage: /** @param { ApplicationWorkerMessage } msg */ (msg) => { 11 | try { 12 | messagePort.postMessage(msg); 13 | } catch { 14 | // Ignroe 15 | } 16 | }, 17 | serializedMocks: workerData.dvlp.serializedMocks, 18 | }); 19 | -------------------------------------------------------------------------------- /src/electron-host/worker-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse `--workerData=` from argv passed to Electron child process 3 | */ 4 | export function getElectronWorkerData() { 5 | const key = '--workerData='; 6 | /** @type { string | undefined } */ 7 | let workerDataArgv; 8 | 9 | for (const arg of process.argv) { 10 | if (arg.startsWith(key)) { 11 | workerDataArgv = arg.slice(key.length); 12 | break; 13 | } 14 | } 15 | 16 | if (workerDataArgv) { 17 | const workerData = /** @type { ElectronProcessWorkerData } */ ( 18 | JSON.parse(Buffer.from(workerDataArgv, 'base64').toString('utf-8')) 19 | ); 20 | workerData.postMessage = (msg) => { 21 | try { 22 | process.send?.(msg); 23 | } catch { 24 | // Ignore 25 | } 26 | }; 27 | 28 | return workerData; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Hooks { 2 | /** 3 | * Bundle non-esm node_modules dependency requested by the browser. 4 | * This hook is run after file read. 5 | */ 6 | onDependencyBundle?( 7 | id: string, 8 | filePath: string, 9 | fileContents: string, 10 | context: DependencyBundleHookContext, 11 | ): Promise | string | undefined; 12 | /** 13 | * Transform file contents for file requested by the browser. 14 | * This hook is run after file read, and before any modifications by dvlp. 15 | */ 16 | onTransform?( 17 | filePath: string, 18 | fileContents: string, 19 | context: TransformHookContext, 20 | ): Promise | string | undefined; 21 | /** 22 | * Manually resolve import specifier. 23 | * This hook is run for each import statement. 24 | * If returns "false", import re-writing is skipped. 25 | * If returns "undefined", import specifier is re-written using default resolver. 26 | * If "context.isDynamic", also possible to return replacement for whole expression. 27 | */ 28 | onResolveImport?( 29 | specifier: string, 30 | context: ResolveHookContext, 31 | defaultResolve: DefaultResolve, 32 | ): string | false | undefined; 33 | /** 34 | * Manually handle response for incoming server request. 35 | * If returns "true", further processing by dvlp will be aborted. 36 | */ 37 | onRequest?( 38 | request: IncomingMessage | Http2ServerRequest, 39 | response: ServerResponse | Http2ServerResponse, 40 | ): Promise | boolean | undefined; 41 | /** 42 | * Modify response body before sending to the browser. 43 | * This hook is run after all modifications by dvlp, and before sending to the browser. 44 | */ 45 | onSend?(filePath: string, responseBody: string): string | undefined; 46 | /** 47 | * Manually resolve import specifiers for application server. 48 | * @see https://nodejs.org/api/esm.html#resolvespecifier-context-nextresolve 49 | */ 50 | onServerResolve?( 51 | specifier: string, 52 | context: { conditions: Array; parentURL?: string }, 53 | nextResolve: NodeResolveLoaderHook, 54 | ): { format?: string; url: string }; 55 | /** 56 | * Transform file contents for application server. 57 | * @see https://nodejs.org/api/esm.html#loadurl-context-nextload 58 | */ 59 | onServerTransform?( 60 | filePath: string, 61 | context: { format?: string }, 62 | nextLoad: NodeLoadLoaderHook, 63 | ): { format: string; source: string | SharedArrayBuffer | Uint8Array }; 64 | } 65 | 66 | declare interface DependencyBundleHookContext { 67 | esbuild: Pick; 68 | } 69 | 70 | declare interface TransformHookContext { 71 | client: { 72 | manufacturer?: string; 73 | name?: string; 74 | ua: string; 75 | version?: string; 76 | }; 77 | esbuild: esbuild; 78 | } 79 | 80 | declare interface ResolveHookContext { 81 | importer: string; 82 | isDynamic: boolean; 83 | } 84 | 85 | declare type DefaultResolve = (specifier: string, importer: string) => string | undefined; 86 | 87 | declare type NodeResolveLoaderHook = ( 88 | specifier: string, 89 | context: { conditions: Array; parentURL?: string }, 90 | nextResolve: NodeResolveLoaderHook, 91 | ) => { format?: string; url: string }; 92 | 93 | declare type NodeLoadLoaderHook = ( 94 | url: string, 95 | context: { format?: string }, 96 | defaultLoad: NodeLoadLoaderHook, 97 | ) => { format: string; source: string | SharedArrayBuffer | Uint8Array }; 98 | -------------------------------------------------------------------------------- /src/hooks/bundle-dependency.js: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 2 | import config from '../config.js'; 3 | import Debug from 'debug'; 4 | import { error } from '../utils/log.js'; 5 | import { getBundleSourcePath } from '../utils/bundling.js'; 6 | import { isBundledFilePath } from '../utils/is.js'; 7 | import { Metrics } from '../utils/metrics.js'; 8 | import { parse } from 'cjs-module-lexer'; 9 | 10 | const debug = Debug('dvlp:bundle'); 11 | 12 | /** 13 | * Bundle node_modules cjs dependency and store at 'filePath' 14 | * 15 | * @param { string } filePath 16 | * @param { Res } res 17 | * @param { Pick } esbuild 18 | * @param { Hooks["onDependencyBundle"] } hookFn 19 | * @returns { Promise } 20 | */ 21 | export async function bundleDependency(filePath, res, esbuild, hookFn) { 22 | if (existsSync(filePath)) { 23 | return; 24 | } 25 | 26 | if (isBundledFilePath(filePath)) { 27 | res.metrics.recordEvent(Metrics.EVENT_NAMES.bundle); 28 | 29 | const [specifier, sourcePath] = getBundleSourcePath(filePath); 30 | let code; 31 | 32 | if (!sourcePath) { 33 | error(`unable to resolve path for module: ${specifier}`); 34 | return; 35 | } 36 | 37 | try { 38 | const sourceContents = readFileSync(sourcePath, 'utf8'); 39 | let entryFilePath = sourcePath; 40 | let entryFileContents = sourceContents; 41 | 42 | if (hookFn) { 43 | code = await hookFn(specifier, entryFilePath, entryFileContents, { 44 | esbuild, 45 | }); 46 | } 47 | 48 | if (code === undefined) { 49 | /** @type { Array } */ 50 | let exports = []; 51 | 52 | try { 53 | ({ exports } = parse(sourceContents)); 54 | } catch { 55 | // ignore 56 | } 57 | 58 | const brokenNamedExports = 59 | config.brokenNamedExportsPackages[specifier] || []; 60 | 61 | // Fix named exports for cjs 62 | if (exports.length > 0 || brokenNamedExports.length > 0) { 63 | const inlineableModulePath = sourcePath.replace(/\\/g, '\\\\'); 64 | const namedExports = new Set([ 65 | 'default', 66 | ...exports, 67 | ...brokenNamedExports, 68 | ]); 69 | namedExports.delete('__esModule'); 70 | const fileContents = `export {${Array.from(namedExports).join( 71 | ', ', 72 | )}} from '${inlineableModulePath}';`; 73 | 74 | entryFilePath = filePath; 75 | entryFileContents = fileContents; 76 | writeFileSync(filePath, fileContents); 77 | } 78 | 79 | const result = await esbuild.build({ 80 | bundle: true, 81 | define: { 'process.env.NODE_ENV': '"development"' }, 82 | entryPoints: [entryFilePath], 83 | format: 'esm', 84 | logLevel: 'error', 85 | mainFields: ['module', 'browser', 'main'], 86 | platform: 'browser', 87 | target: 'es2018', 88 | write: false, 89 | }); 90 | 91 | if (!result.outputFiles) { 92 | throw Error(`unknown bundling error: ${result.warnings.join('\n')}`); 93 | } 94 | code = result.outputFiles[0].text; 95 | } 96 | } catch (err) { 97 | debug(`error bundling "${specifier}"`); 98 | res.writeHead(500); 99 | res.end(/** @type { Error } */ (err).message); 100 | error(err); 101 | return; 102 | } 103 | 104 | if (code !== undefined) { 105 | debug(`bundled content for ${specifier}`); 106 | writeFileSync(filePath, code); 107 | res.bundled = true; 108 | } 109 | 110 | res.metrics.recordEvent(Metrics.EVENT_NAMES.bundle); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import { error, noisyWarn } from '../utils/log.js'; 2 | import { bundleDependency } from './bundle-dependency.js'; 3 | import chalk from 'chalk'; 4 | import esbuild from 'esbuild'; 5 | import { isNodeModuleFilePath } from '../utils/is.js'; 6 | import { resolve } from '../resolver/index.js'; 7 | import { transform } from './transform.js'; 8 | 9 | const HOOK_NAMES = [ 10 | 'onDependencyBundle', 11 | 'onTransform', 12 | 'onResolveImport', 13 | 'onRequest', 14 | 'onSend', 15 | 'onServerResolve', 16 | 'onServerTransform', 17 | ]; 18 | 19 | export class Hooker { 20 | /** 21 | * Constructor 22 | * 23 | * @param { Hooks } [hooks] 24 | * @param { Watcher } [watcher] 25 | */ 26 | constructor(hooks, watcher) { 27 | if (hooks) { 28 | for (const name of Object.keys(hooks)) { 29 | if (!HOOK_NAMES.includes(name) && name !== 'filePath') { 30 | noisyWarn( 31 | `${chalk.yellow( 32 | '⚠️', 33 | )} no hook named "${name}". Valid hooks include: ${HOOK_NAMES.join( 34 | ', ', 35 | )}`, 36 | ); 37 | } 38 | } 39 | } 40 | 41 | /** @type { Hooks | undefined } */ 42 | this.hooks = hooks; 43 | /** @type { Map } */ 44 | this.transformCache = new Map(); 45 | this.watcher = watcher; 46 | 47 | // Patch build to watch files when used in transform hook, 48 | // since esbuild file reads don't use fs.readFile API 49 | if (watcher) { 50 | /** @type { import('esbuild').Plugin } */ 51 | const resolvePlugin = { 52 | name: 'watch-project-files', 53 | setup(build) { 54 | build.onResolve({ filter: /^[./]/ }, function (args) { 55 | const { importer, path } = args; 56 | const filePath = importer ? resolve(path, importer) : path; 57 | 58 | if (filePath && !isNodeModuleFilePath(filePath)) { 59 | watcher && watcher.add(filePath); 60 | } 61 | 62 | return undefined; 63 | }); 64 | }, 65 | }; 66 | this.patchedESBuild = new Proxy(esbuild.build, { 67 | apply(target, context, args) { 68 | if (!args[0].plugins) { 69 | args[0].plugins = []; 70 | } 71 | args[0].plugins.unshift(resolvePlugin); 72 | return Reflect.apply(target, context, args); 73 | }, 74 | }); 75 | } else { 76 | this.patchedESBuild = esbuild.build; 77 | } 78 | 79 | this.bundleDependency = this.bundleDependency.bind(this); 80 | this.transform = this.transform.bind(this); 81 | this.resolveImport = this.resolveImport.bind(this); 82 | this.send = this.send.bind(this); 83 | } 84 | 85 | /** 86 | * Bundle node_modules cjs dependency and store at 'filePath' 87 | * 88 | * @param { string } filePath 89 | * @param { Res } res 90 | * @returns { Promise } 91 | */ 92 | async bundleDependency(filePath, res) { 93 | await bundleDependency( 94 | filePath, 95 | res, 96 | { 97 | build: esbuild.build, 98 | }, 99 | this.hooks && this.hooks.onDependencyBundle, 100 | ); 101 | } 102 | 103 | /** 104 | * Transform file content for requested 'filePath' 105 | * 106 | * @param { string } filePath 107 | * @param { string } lastChangedFilePath 108 | * @param { Res } res 109 | * @param { TransformHookContext["client"] } clientPlatform 110 | * @returns { Promise } 111 | */ 112 | async transform(filePath, lastChangedFilePath, res, clientPlatform) { 113 | await transform( 114 | filePath, 115 | lastChangedFilePath, 116 | res, 117 | clientPlatform, 118 | this.transformCache, 119 | { 120 | build: this.patchedESBuild, 121 | transform: esbuild.transform, 122 | }, 123 | this.hooks && this.hooks.onTransform, 124 | ); 125 | } 126 | 127 | /** 128 | * Resolve module import 'specifier' 129 | * 130 | * @param { string } specifier 131 | * @param { ResolveHookContext } context 132 | * @param { DefaultResolve } defaultResolve 133 | * @returns { string | false | undefined} 134 | */ 135 | resolveImport(specifier, context, defaultResolve) { 136 | let result; 137 | 138 | if (this.hooks && this.hooks.onResolveImport) { 139 | result = this.hooks.onResolveImport(specifier, context, defaultResolve); 140 | } 141 | if (result === undefined) { 142 | result = defaultResolve(specifier, context.importer); 143 | } 144 | 145 | return result; 146 | } 147 | 148 | /** 149 | * Allow external response handling 150 | * 151 | * @param { Req } req 152 | * @param { Res } res 153 | * @returns { Promise } 154 | */ 155 | async handleRequest(req, res) { 156 | if (this.hooks && this.hooks.onRequest) { 157 | try { 158 | // Check if finished in case no return value 159 | if ((await this.hooks.onRequest(req, res)) || res.finished) { 160 | return true; 161 | } 162 | } catch (err) { 163 | res.writeHead(500); 164 | res.end(/** @type { Error } */ (err).message); 165 | error(err); 166 | return true; 167 | } 168 | } 169 | 170 | return false; 171 | } 172 | 173 | /** 174 | * Allow modification of 'filePath' content before sending the request 175 | * 176 | * @param { string } filePath 177 | * @param { string } fileContents 178 | * @returns { string } 179 | */ 180 | send(filePath, fileContents) { 181 | let result; 182 | 183 | if (this.hooks && this.hooks.onSend) { 184 | result = this.hooks.onSend(filePath, fileContents); 185 | } 186 | 187 | return result || fileContents; 188 | } 189 | 190 | /** 191 | * Destroy instance 192 | */ 193 | destroy() { 194 | this.transformCache.clear(); 195 | this.watcher = undefined; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/hooks/transform.js: -------------------------------------------------------------------------------- 1 | import { findClosest, getProjectPath, getTypeFromPath } from '../utils/file.js'; 2 | import Debug from 'debug'; 3 | import { error } from '../utils/log.js'; 4 | import { extname } from 'node:path'; 5 | import { getType } from '../utils/mime.js'; 6 | import { isTransformableJsFile } from '../utils/is.js'; 7 | import { Metrics } from '../utils/metrics.js'; 8 | import { parseEsbuildTarget } from '../utils/platform.js'; 9 | import { readFileSync } from 'node:fs'; 10 | 11 | const debug = Debug('dvlp:transform'); 12 | const tsconfigPath = findClosest('tsconfig.json'); 13 | const tsconfig = tsconfigPath 14 | ? readFileSync(tsconfigPath, 'utf8') 15 | : `{ 16 | compilerOptions: { 17 | useDefineForClassFields: true, 18 | }, 19 | }`; 20 | 21 | /** 22 | * Transform file content for request for 'filePath' 23 | * 24 | * @param { string } filePath 25 | * @param { string } lastChangedFilePath 26 | * @param { Res } res 27 | * @param { TransformHookContext["client"] } clientPlatform 28 | * @param { Map } cache 29 | * @param { esbuild } esbuild 30 | * @param { Hooks["onTransform"] } hookFn 31 | * @returns { Promise } 32 | */ 33 | export async function transform( 34 | filePath, 35 | lastChangedFilePath, 36 | res, 37 | clientPlatform, 38 | cache, 39 | esbuild, 40 | hookFn, 41 | ) { 42 | res.metrics.recordEvent(Metrics.EVENT_NAMES.transform); 43 | 44 | // Segment cache by user agent to support different transforms based on client 45 | const cacheKey = `${clientPlatform.ua}:${filePath}`; 46 | const lastChangedCacheKey = `${clientPlatform.ua}:${lastChangedFilePath}`; 47 | const relativeFilePath = getProjectPath(filePath); 48 | const fileType = getTypeFromPath(filePath); 49 | const fileExtension = extname(filePath); 50 | // Dependencies that are concatenated during transform aren't cached, 51 | // but they are watched when read from file system during transformation, 52 | // so transform again if changed file is of same type 53 | const lastChangedIsDependency = 54 | lastChangedFilePath && 55 | !cache.has(lastChangedCacheKey) && 56 | getTypeFromPath(lastChangedFilePath) === fileType; 57 | let code = cache.get(cacheKey); 58 | let transformed = false; 59 | 60 | if (lastChangedIsDependency || lastChangedFilePath === filePath || !code) { 61 | try { 62 | const fileContents = readFileSync(filePath, 'utf8'); 63 | code = undefined; 64 | 65 | if (hookFn !== undefined) { 66 | code = await hookFn(filePath, fileContents, { 67 | client: clientPlatform, 68 | esbuild, 69 | }); 70 | } 71 | if (code === undefined) { 72 | // Skip default transform if not necessary 73 | if (!isTransformableJsFile(filePath, fileContents)) { 74 | return; 75 | } 76 | 77 | /** @type { import("esbuild").TransformOptions } */ 78 | const options = { 79 | format: 'esm', 80 | // @ts-expect-error - filtered by "fileType" 81 | loader: fileExtension.slice(1), 82 | logLevel: 'warning', 83 | sourcefile: filePath, 84 | target: parseEsbuildTarget(clientPlatform), 85 | }; 86 | 87 | if (tsconfig) { 88 | options.tsconfigRaw = tsconfig; 89 | } 90 | 91 | code = (await esbuild.transform(fileContents, options)).code; 92 | } 93 | if (code !== undefined) { 94 | transformed = true; 95 | cache.set(cacheKey, code); 96 | } 97 | } catch (err) { 98 | debug(`error transforming "${relativeFilePath}"`); 99 | res.writeHead(500); 100 | res.end(/** @type { Error } */ (err).message); 101 | error(err); 102 | return; 103 | } 104 | } 105 | 106 | if (code !== undefined) { 107 | debug( 108 | `${ 109 | transformed ? 'transformed content for' : 'skipping transform for' 110 | } "${relativeFilePath}"`, 111 | ); 112 | res.transformed = true; 113 | res.writeHead(200, { 114 | 'Access-Control-Allow-Origin': '*', 115 | 'Content-Length': Buffer.byteLength(code), 116 | 'Content-Type': getType(filePath) || undefined, 117 | }); 118 | res.end(code); 119 | res.metrics.recordEvent(Metrics.EVENT_NAMES.transform); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/mock/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Mocks { 2 | addResponse( 3 | req: string | MockRequest, 4 | res: MockResponse | MockResponseHandler, 5 | once?: boolean, 6 | onMock?: () => void, 7 | ): () => void; 8 | addPushEvents( 9 | stream: string | MockPushStream, 10 | events: MockPushEvent | Array, 11 | ): () => void; 12 | load(filePaths: string | Array): void; 13 | matchResponse(href: string, req?: Req, res?: Res): boolean | MockResponseData; 14 | matchPushEvent( 15 | stream: string | MockPushStream, 16 | name: string, 17 | push: (stream: string | PushStream, event: PushEvent) => void, 18 | ): boolean; 19 | hasMatch( 20 | reqOrMockData: 21 | | string 22 | | URL 23 | | { url: string } 24 | | MockResponseData 25 | | MockStreamData, 26 | ): boolean; 27 | remove( 28 | reqOrMockData: 29 | | string 30 | | URL 31 | | { url: string } 32 | | MockResponseData 33 | | MockStreamData, 34 | ): void; 35 | clear(): void; 36 | /** @deprecated */ 37 | clean(): void; 38 | } 39 | 40 | declare type MockResponseDataType = 'html' | 'file' | 'json'; 41 | declare type MockStreamDataType = 'ws' | 'es'; 42 | 43 | declare interface MockResponseData { 44 | url: URL; 45 | originRegex: RegExp; 46 | pathRegex: RegExp; 47 | paramsMatch: import('path-to-regexp').MatchFunction; 48 | searchParams: URLSearchParams; 49 | ignoreSearch: boolean; 50 | once: boolean; 51 | filePath: string; 52 | type: MockResponseDataType; 53 | response: MockResponse | MockResponseHandler; 54 | callback?: () => void; 55 | } 56 | 57 | declare interface MockStreamEventData { 58 | name?: string; 59 | message: Buffer | string | Record; 60 | options: MockPushEventOptions & { 61 | protocol?: string; 62 | }; 63 | } 64 | 65 | declare interface MockStreamData { 66 | url: URL; 67 | originRegex: RegExp; 68 | pathRegex: RegExp; 69 | paramsMatch: import('path-to-regexp').MatchFunction; 70 | searchParams: URLSearchParams; 71 | ignoreSearch: boolean; 72 | filePath: string; 73 | type: MockStreamDataType; 74 | protocol: string; 75 | events: { [name: string]: Array }; 76 | } 77 | 78 | declare interface MockRequest { 79 | url: string; 80 | filePath?: string; 81 | ignoreSearch?: boolean; 82 | } 83 | 84 | declare type MockResponseHandler = (req: Req, res: Res) => void; 85 | 86 | declare interface MockResponse { 87 | body: string | Record; 88 | hang?: boolean; 89 | headers?: Record; 90 | error?: boolean; 91 | missing?: boolean; 92 | offline?: boolean; 93 | status?: number; 94 | } 95 | 96 | declare interface MockResponseJSONSchema { 97 | request: MockRequest; 98 | response: MockResponse; 99 | } 100 | 101 | declare interface MockPushEventJSONSchema { 102 | stream: MockPushStream; 103 | events: Array; 104 | } 105 | 106 | declare interface MockPushStream { 107 | url: string; 108 | type: string; 109 | filePath?: string; 110 | ignoreSearch?: boolean; 111 | protocol?: string; 112 | } 113 | 114 | declare interface MockPushEventOptions { 115 | delay?: number; 116 | connect?: boolean; 117 | event?: string; 118 | id?: string; 119 | namespace?: string; 120 | } 121 | 122 | declare interface MockPushEvent { 123 | name: string; 124 | message?: Buffer | string | Record; 125 | sequence?: Array; 126 | options?: MockPushEventOptions; 127 | } 128 | 129 | declare interface SerializedMock { 130 | href: string; 131 | originRegex: string; 132 | pathRegex: string; 133 | search: string; 134 | ignoreSearch: boolean; 135 | events?: Array; 136 | } 137 | 138 | declare interface DeserializedMock { 139 | href: string; 140 | originRegex: RegExp; 141 | pathRegex: RegExp; 142 | search: URLSearchParams; 143 | ignoreSearch: boolean; 144 | events?: Array; 145 | } 146 | -------------------------------------------------------------------------------- /src/push-events/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface PushClient { 2 | on(event: string, callback: (event: { data: string }) => void): void; 3 | send(msg: Buffer | string, options?: PushEventOptions): void; 4 | removeAllListeners(): void; 5 | close(): void; 6 | } 7 | 8 | declare interface PushStream { 9 | url: string; 10 | type: string; 11 | } 12 | 13 | declare interface PushEvent { 14 | message: Buffer | string | Record; 15 | options?: PushEventOptions; 16 | } 17 | 18 | declare interface PushEventOptions { 19 | id?: string; // EventSource ID 20 | event?: string; // EventSource event OR Socket.IO event 21 | namespace?: string; // Socket.IO namespace 22 | protocol?: string; // Socket.IO protocol 23 | } 24 | -------------------------------------------------------------------------------- /src/reload/event-source.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events'; 2 | 3 | const DEFAULT_PING = 15 * 1000; 4 | const DEFAULT_RETRY = 5 * 1000; 5 | /** @enum { number } */ 6 | const READY_STATE = { 7 | CONNECTING: 0, 8 | OPEN: 1, 9 | CLOSING: 2, 10 | CLOSED: 3, 11 | }; 12 | 13 | export class EventSource extends EventEmitter { 14 | /** 15 | * Determine if "req" is an EventSource request 16 | * 17 | * @param { IncomingMessage | Http2ServerRequest } req 18 | */ 19 | static isEventSource(req) { 20 | return ( 21 | req.method === 'GET' && 22 | req.headers.accept !== undefined && 23 | req.headers.accept.includes('text/event-stream') 24 | ); 25 | } 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @param { IncomingMessage | Http2ServerRequest } req 31 | * @param { ServerResponse | Http2ServerResponse } res 32 | */ 33 | constructor(req, res) { 34 | super(); 35 | this.readyState = READY_STATE.CONNECTING; 36 | this._res = res; 37 | 38 | if (res.finished) { 39 | return; 40 | } 41 | 42 | req.socket.setKeepAlive(true); 43 | if (!res.hasHeader('Access-Control-Allow-Origin')) { 44 | res.setHeader('Access-Control-Allow-Origin', '*'); 45 | } 46 | res.writeHead(200, { 47 | 'Content-Type': 'text/event-stream', 48 | 'Cache-Control': 'no-cache, no-store', 49 | }); 50 | 51 | this._write(`retry: ${Math.floor(DEFAULT_RETRY)}\r\n\r\n`); 52 | this._pingIntervalId = setInterval(() => { 53 | this.ping(); 54 | }, DEFAULT_PING); 55 | 56 | for (const event of ['close', 'error']) { 57 | req.on(event, () => { 58 | this.close(); 59 | }); 60 | } 61 | 62 | process.nextTick(() => this._open()); 63 | } 64 | 65 | /** 66 | * Send optional message and close the connection 67 | * 68 | * @param { string } [message] 69 | */ 70 | end(message) { 71 | if (message) { 72 | this.send(message); 73 | } 74 | this.close(); 75 | } 76 | 77 | /** 78 | * Send message 79 | * 80 | * @param { string } message 81 | * @param { { event?: string, id?: string } } [options] 82 | */ 83 | send(message, options = {}) { 84 | if (this.readyState > READY_STATE.OPEN) { 85 | return false; 86 | } 87 | 88 | const { event, id } = options; 89 | const data = message.replace(/(\r\n|\r|\n)/g, '$1data: '); 90 | let frame = ''; 91 | 92 | if (event) { 93 | frame += `event: ${event}\r\n`; 94 | } 95 | if (id) { 96 | frame += `id: ${id}\r\n`; 97 | } 98 | frame += `data: ${data}\r\n\r\n`; 99 | 100 | return this._write(frame); 101 | } 102 | 103 | /** 104 | * Ping client 105 | */ 106 | ping() { 107 | return this._write(':\r\n\r\n'); 108 | } 109 | 110 | /** 111 | * Close the connection 112 | */ 113 | close() { 114 | if (this.readyState > READY_STATE.OPEN) { 115 | return false; 116 | } 117 | 118 | this.readyState = READY_STATE.CLOSING; 119 | 120 | if (this._pingIntervalId) { 121 | clearInterval(this._pingIntervalId); 122 | } 123 | this._res.end(); 124 | // @ts-expect-error - clean up 125 | this._res = undefined; 126 | 127 | this.emit('close'); 128 | 129 | this.readyState = READY_STATE.CLOSED; 130 | 131 | return true; 132 | } 133 | 134 | /** 135 | * @private 136 | */ 137 | _open() { 138 | if (this.readyState !== READY_STATE.CONNECTING) { 139 | return; 140 | } 141 | 142 | this.readyState = READY_STATE.OPEN; 143 | this.emit('open'); 144 | } 145 | 146 | /** 147 | * @param { string } chunk 148 | * @returns { boolean } 149 | * @private 150 | */ 151 | _write(chunk) { 152 | try { 153 | // @ts-expect-error - writeable 154 | this._res.write(chunk); 155 | return true; 156 | } catch { 157 | this.close(); 158 | return false; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/reload/reload-client-embed.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | 5 | const reloadClient = 6 | // @ts-expect-error - global 7 | global.$RELOAD_CLIENT || 8 | fs.readFileSync( 9 | path.join(path.dirname(fileURLToPath(import.meta.url)), 'reload-client.js'), 10 | 'utf8', 11 | ); 12 | 13 | /** 14 | * Retrieve embeddable reload client script 15 | * 16 | * @param { number } port 17 | */ 18 | export function getReloadClientEmbed(port) { 19 | return reloadClient.replace(/\$RELOAD_PATHNAME/g, '/dvlp/reload'); 20 | } 21 | -------------------------------------------------------------------------------- /src/resolver/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Package { 2 | browser?: Record; 3 | env: 'browser' | 'node'; 4 | exports?: string | Record>; 5 | exportsConditions: Array; 6 | imports?: string | Record>; 7 | isProjectPackage: boolean; 8 | manifestPath: string; 9 | main?: string; 10 | name: string; 11 | path: string; 12 | paths: Array; 13 | type: 'module' | 'commonjs' | undefined; 14 | version: string; 15 | } 16 | 17 | declare type ResolveResult = { filePath: string; format: Package['type']; url?: string }; 18 | -------------------------------------------------------------------------------- /src/resolver/utils.js: -------------------------------------------------------------------------------- 1 | import { isBareSpecifier } from '../utils/is.js'; 2 | 3 | /** 4 | * Retrieve package name from "specifier" 5 | * 6 | * @param { string } specifier 7 | * @returns { string | undefined } 8 | */ 9 | export function getPackageNameFromSpecifier(specifier) { 10 | if (isBareSpecifier(specifier)) { 11 | const segments = specifier.split('/'); 12 | let name = segments[0]; 13 | 14 | if (name.startsWith('@')) { 15 | name += `/${segments[1]}`; 16 | } 17 | 18 | return name; 19 | } 20 | } 21 | 22 | /** 23 | * Determine whether "specifier" is self-referential based on "pkg" 24 | * 25 | * @param { string } specifier 26 | * @param { Package } pkg 27 | * @returns { boolean } 28 | */ 29 | export function isSelfReferentialSpecifier(specifier, pkg) { 30 | return getPackageNameFromSpecifier(specifier) === pkg.name; 31 | } 32 | -------------------------------------------------------------------------------- /src/server/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ServerOptions { 2 | /** 3 | * The command-line arguments to pass to the application thread or electron process (default `[]`). 4 | */ 5 | argv?: Array; 6 | /** 7 | * The path or glob pattern containing ".crt" and ".key" files. 8 | * This enables secure https mode by proxying all requests through a secure server (default `''`). 9 | */ 10 | certsPath?: string | Array; 11 | /** 12 | * Additional directories to use for resolving file requests (default `[]`). 13 | */ 14 | directories?: Array; 15 | /** 16 | * Run file as electron.js entry file (default `false`). 17 | */ 18 | electron?: boolean; 19 | /** 20 | * The path to a custom hooks registration file (default `''`). 21 | */ 22 | hooksPath?: string; 23 | /** 24 | * The path(s) to load mock files from. 25 | */ 26 | mockPath?: string | Array; 27 | /** 28 | * Port to expose on `localhost`. 29 | * Will use `process.env.PORT` if not specified here (default `8080`). 30 | */ 31 | port?: number; 32 | /** 33 | * Enable/disable browser reloading (default `true`). 34 | */ 35 | reload?: boolean; 36 | /** 37 | * Disable/enable all logging (default `false`). 38 | */ 39 | silent?: boolean; 40 | /** 41 | * Disable/enable verbose logging (default `false`). 42 | */ 43 | verbose?: boolean; 44 | } 45 | 46 | declare interface Server { 47 | /** 48 | * The entry config 49 | */ 50 | readonly entry: Entry; 51 | /** 52 | * The listening state 53 | */ 54 | readonly isListening: boolean; 55 | /** 56 | * The localhost origin 57 | */ 58 | readonly origin: string; 59 | /** 60 | * The `Mocks` instance 61 | */ 62 | readonly mocks: Mocks; 63 | /** 64 | * The localhost port number 65 | */ 66 | readonly port: number; 67 | /** 68 | * The active application worker thread, if initialised 69 | */ 70 | readonly applicationWorker?: ApplicationWorker; 71 | /** 72 | * The active electron process, if initialised 73 | */ 74 | readonly electronProcess?: ElectronProcess; 75 | /** 76 | * Add `filePaths` to file watcher 77 | */ 78 | addWatchFiles(filePaths: string | Array): void; 79 | /** 80 | * Destroy server instance 81 | */ 82 | destroy(): Promise; 83 | } 84 | -------------------------------------------------------------------------------- /src/server/certificate-validation.js: -------------------------------------------------------------------------------- 1 | import { fatal, noisyWarn } from '../utils/log.js'; 2 | import { Certificate } from '@fidm/x509'; 3 | import chalk from 'chalk'; 4 | import fs from 'node:fs'; 5 | import { getDirectoryContents } from '../utils/file.js'; 6 | import path from 'node:path'; 7 | 8 | /** 9 | * Find cert/key 10 | * 11 | * @param { string | Array } certsPaths 12 | * @returns { { cert: Buffer, key: Buffer } } 13 | */ 14 | export function resolveCerts(certsPaths) { 15 | if (!Array.isArray(certsPaths)) { 16 | certsPaths = [certsPaths]; 17 | } 18 | 19 | let cert; 20 | let key; 21 | 22 | for (const certsPath of certsPaths) { 23 | for (const filePath of getDirectoryContents(certsPath)) { 24 | const extname = path.extname(filePath); 25 | 26 | if ( 27 | !cert && 28 | (extname === '.crt' || extname === '.cert') && 29 | !filePath.endsWith('.issuer.crt') 30 | ) { 31 | cert = fs.readFileSync(filePath); 32 | } else if (!key && extname === '.key') { 33 | key = fs.readFileSync(filePath); 34 | } 35 | } 36 | } 37 | 38 | if (!cert || !key) { 39 | throw Error( 40 | `unable to find .crt or .key file after searching "${certsPaths.join( 41 | ', ', 42 | )}"`, 43 | ); 44 | } 45 | 46 | return { cert, key }; 47 | } 48 | 49 | /** 50 | * Validate cert file data and return CommonName 51 | * 52 | * @param { Buffer} certFileData 53 | * @returns { string | undefined } 54 | */ 55 | export function validateCert(certFileData) { 56 | try { 57 | const cert = Certificate.fromPEM(certFileData); 58 | const { 59 | subject: { commonName }, 60 | validTo, 61 | } = cert; 62 | const now = new Date(); 63 | const expires = new Date(validTo); 64 | const diff = expires.getTime() - now.getTime(); 65 | 66 | if (diff < 10) { 67 | fatal('ssl certificate has expired!\n'); 68 | } else if (diff / 86400000 < 10) { 69 | noisyWarn( 70 | `\n ${chalk.yellow('⚠️ ssl certificate will expire soon!')}\n`, 71 | ); 72 | } 73 | 74 | return commonName; 75 | } catch (err) { 76 | fatal(err); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/server/handlers.js: -------------------------------------------------------------------------------- 1 | import { connectClient, pushEvent } from '../push-events/index.js'; 2 | import chalk from 'chalk'; 3 | import config from '../config.js'; 4 | import { EventSource } from '../reload/event-source.js'; 5 | import favicon from '../utils/favicon.js'; 6 | import { find } from '../utils/file.js'; 7 | import { fromBase64Url } from '../utils/base64Url.js'; 8 | import { noisyInfo } from '../utils/log.js'; 9 | import { send } from '../utils/send.js'; 10 | // @ts-expect-error - no types 11 | import WebSocket from 'faye-websocket'; 12 | 13 | const favIcon = Buffer.from(favicon, 'base64'); 14 | 15 | /** 16 | * Handle request for favicon 17 | * Returns 'true' if handled 18 | * 19 | * @param { Req } req 20 | * @param { Res } res 21 | * @returns { boolean } 22 | */ 23 | export function handleFavicon(req, res) { 24 | if (req.url.includes('/favicon.ico')) { 25 | const customFavIcon = find(req); 26 | 27 | if (customFavIcon) { 28 | res.setHeader('Cache-Coontrol', `public, max-age=${config.maxAge}`); 29 | send(customFavIcon, res); 30 | } else { 31 | res.writeHead(200, { 32 | 'Content-Length': favIcon.length, 33 | 'Cache-Control': `public, max-age=${60 * 10}`, 34 | 'Content-Type': 'image/x-icon;charset=UTF-8', 35 | }); 36 | res.end(favIcon); 37 | } 38 | 39 | return true; 40 | } 41 | 42 | return false; 43 | } 44 | 45 | /** 46 | * Handle mock responses, including EventSource connection 47 | * Returns 'true' if handled 48 | * 49 | * @param { Req } req 50 | * @param { Res } res 51 | * @param { Mocks } [mocks] 52 | * @returns { boolean } 53 | */ 54 | export function handleMockResponse(req, res, mocks) { 55 | if (mocks !== undefined) { 56 | const url = new URL(req.url, `http://localhost:${config.activePort}`); 57 | let mockParam = url.searchParams.get('dvlpmock'); 58 | 59 | if (mockParam) { 60 | mockParam = decodeURIComponent(mockParam); 61 | 62 | if (EventSource.isEventSource(req)) { 63 | connectClient( 64 | { 65 | url: mockParam, 66 | type: 'es', 67 | }, 68 | req, 69 | res, 70 | ); 71 | // Send 'connect' event if it exists 72 | mocks.matchPushEvent(mockParam, 'connect', pushEvent); 73 | noisyInfo( 74 | `${chalk.green( 75 | ' 0ms', 76 | )} connected to EventSource client at ${chalk.green(mockParam)}`, 77 | ); 78 | } else { 79 | mocks.matchResponse(mockParam, req, res); 80 | } 81 | 82 | return true; 83 | } else if (mocks.hasMatch(req)) { 84 | const handled = mocks.matchResponse(req.url, req, res); 85 | 86 | return handled === true; 87 | } 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /** 94 | * Handle mock WebSocket connection 95 | * 96 | * @param { Req } req 97 | * @param { object } socket 98 | * @param { object } body 99 | * @param { Mocks } mocks 100 | * @returns { void } 101 | */ 102 | export function handleMockWebSocket(req, socket, body, mocks) { 103 | const url = new URL(req.url, `http://localhost:${config.activePort}`); 104 | let mockPath = url.searchParams.get('dvlpmock'); 105 | 106 | if (mockPath && WebSocket.isWebSocket(req)) { 107 | mockPath = decodeURIComponent(mockPath); 108 | connectClient( 109 | { 110 | url: mockPath, 111 | type: 'ws', 112 | }, 113 | req, 114 | socket, 115 | body, 116 | ); 117 | // Send 'connect' event if it exists 118 | mocks.matchPushEvent(mockPath, 'connect', pushEvent); 119 | noisyInfo( 120 | `${chalk.green( 121 | ' 0ms', 122 | )} connected to WebSocket client at ${chalk.green(mockPath)}`, 123 | ); 124 | } 125 | } 126 | 127 | /** 128 | * Handle push event request 129 | * Returns 'true' if handled 130 | * 131 | * @param { Req } req 132 | * @param { Res } res 133 | * @param { Mocks } [mocks] 134 | * @returns { boolean } 135 | */ 136 | export function handlePushEvent(req, res, mocks) { 137 | if (mocks !== undefined) { 138 | if (req.method === 'POST' && req.url === '/dvlp/push-event') { 139 | let body = ''; 140 | 141 | req.on('data', (chunk) => { 142 | body += chunk.toString(); 143 | }); 144 | req.on('end', () => { 145 | const { stream, event } = JSON.parse(body); 146 | 147 | if (typeof event === 'string') { 148 | mocks.matchPushEvent(stream, event, pushEvent); 149 | } else { 150 | pushEvent(stream, event); 151 | } 152 | 153 | res.writeHead(200); 154 | res.end('ok'); 155 | }); 156 | 157 | return true; 158 | } 159 | } 160 | 161 | return false; 162 | } 163 | 164 | /** 165 | * Handle file request 166 | * 167 | * @param { string } filePath 168 | * @param { Res } res 169 | */ 170 | export function handleFile(filePath, res) { 171 | send(filePath, res); 172 | } 173 | 174 | /** 175 | * Handle request for data URL (?dvlpdata=) 176 | * Returns 'true' if handled 177 | * 178 | * @param { Req } req 179 | * @param { Res } res 180 | * @returns { boolean } 181 | */ 182 | export function handleDataUrl(req, res) { 183 | const url = new URL(req.url, `http://localhost:${config.activePort}`); 184 | const data = url.searchParams.get('dvlpdata'); 185 | 186 | if (data) { 187 | const html = fromBase64Url(data); 188 | 189 | if (!res.hasHeader('Access-Control-Allow-Origin')) { 190 | res.setHeader('Access-Control-Allow-Origin', '*'); 191 | } 192 | res.writeHead(200, { 193 | 'Content-Length': Buffer.byteLength(html, 'utf-8'), 194 | 'Content-Type': 'text/html;charset=UTF-8', 195 | }); 196 | res.end(html); 197 | 198 | return true; 199 | } 200 | 201 | return false; 202 | } 203 | -------------------------------------------------------------------------------- /src/test-browser/index.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck - browser code 2 | import '../mock/mock-client.js'; 3 | 4 | export const testBrowser = { 5 | /** 6 | * Disable all external network connections 7 | * and optionally reroute all external requests to this server 8 | * 9 | * @param { boolean } [rerouteAllRequests] 10 | */ 11 | disableNetwork(rerouteAllRequests) { 12 | return window.dvlp.disableNetwork(rerouteAllRequests); 13 | }, 14 | /** 15 | * Re-enable all external network connections 16 | */ 17 | enableNetwork() { 18 | return window.dvlp.enableNetwork(); 19 | }, 20 | /** 21 | * Add mock response for "req" 22 | * 23 | * @param { string | MockRequest } req 24 | * @param { MockResponse } [res] 25 | * @param { boolean } [once] 26 | * @param { () => void } [onMockCallback] 27 | * @returns { () => void } remove mock instance 28 | */ 29 | mockResponse(req, res, once, onMockCallback) { 30 | return window.dvlp.mockResponse(req, res, once, onMockCallback); 31 | }, 32 | /** 33 | * Register mock push "events" for "stream" 34 | * 35 | * @param { string | MockPushStream } stream 36 | * @param { MockPushEvent | Array } events 37 | * @param { (data: any) => void } [onSendCallback] 38 | * @returns { () => void } remove mock instance 39 | */ 40 | mockPushEvents(stream, events, onSendCallback) { 41 | return window.dvlp.mockPushEvents(stream, events, onSendCallback); 42 | }, 43 | /** 44 | * Trigger EventSource/WebSocket event 45 | * 46 | * @param { string } stream 47 | * @param { string | { message: string | object, options: { event: string, id: string } } } event 48 | */ 49 | pushEvent(stream, event) { 50 | return window.dvlp.pushEvent(stream, event); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/test-server/_.d.ts: -------------------------------------------------------------------------------- 1 | declare interface TestServerOptions { 2 | /** 3 | * Enable/disable automatic dummy responses. 4 | * If unable to resolve a request to a local file or mock, 5 | * the server will respond with a dummy file of the appropriate type (default `true`). 6 | */ 7 | autorespond?: boolean; 8 | /** 9 | * The amount of artificial latency to introduce (in `ms`) for responses (default `50`). 10 | */ 11 | latency?: number; 12 | /** 13 | * The port to expose on `localhost`. Will use `process.env.PORT` if not specified here (default `8080`). 14 | */ 15 | port?: number; 16 | /** 17 | * The subpath from `process.cwd()` to prepend to relative paths (default `''`). 18 | */ 19 | webroot?: string; 20 | } 21 | 22 | declare class TestServer { 23 | latency: number; 24 | port: number; 25 | mocks: Mocks; 26 | webroot: string; 27 | constructor(options?: TestServerOptions); 28 | /** 29 | * Load mock files at `filePath` 30 | */ 31 | loadMockFiles(filePath: string | Array): void; 32 | /** 33 | * Register mock `response` for `request`. 34 | * If `once`, mock will be unregistered after first use. 35 | * If `onMock`, callback when response is mocked 36 | */ 37 | mockResponse( 38 | request: string | MockRequest, 39 | response: MockResponse | MockResponseHandler, 40 | once?: boolean, 41 | onMockCallback?: () => void, 42 | ): void; 43 | /** 44 | * Register mock push `events` for `stream` 45 | */ 46 | mockPushEvents( 47 | stream: string | MockPushStream, 48 | events: MockPushEvent | Array, 49 | onSendCallback?: (data: any) => void, 50 | ): void; 51 | /** 52 | * Push data to WebSocket/EventSource clients 53 | * A string passed as `event` will be handled as a named mock push event 54 | */ 55 | pushEvent(stream: string | PushStream, event?: string | PushEvent): void; 56 | /** 57 | * Clear all mock data 58 | */ 59 | clearMockFiles(): void; 60 | /** 61 | * Prevent process from exiting while this server is active 62 | */ 63 | ref(): void; 64 | /** 65 | * Allow process to exit if this is the only active 66 | */ 67 | unref(): void; 68 | /** 69 | * Destroy server instance 70 | */ 71 | destroy(): Promise; 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/_.d.ts: -------------------------------------------------------------------------------- 1 | declare class Metrics { 2 | events: Map; 3 | constructor(res: Res); 4 | recordEvent(name: string): void; 5 | getEvent(name: string, formatted?: boolean): string | number; 6 | } 7 | declare namespace Metrics { 8 | export enum EVENT_NAMES { 9 | bundle = 'bundle file', 10 | csp = 'inject CSP header', 11 | imports = 'rewrite imports', 12 | mock = 'mock response', 13 | response = 'response', 14 | scripts = 'inject HTML scripts', 15 | transpile = 'transpile file', 16 | } 17 | } 18 | 19 | declare interface PatchResponseOptions { 20 | directories?: Array; 21 | footerScript?: { 22 | string: string; 23 | url?: string; 24 | }; 25 | headerScript?: { 26 | string: string; 27 | url?: string; 28 | }; 29 | send?(filePath: string, responseBody: string): string | undefined; 30 | resolveImport?( 31 | specifier: string, 32 | context: ResolveHookContext, 33 | defaultResolve: DefaultResolve, 34 | ): string | false | undefined; 35 | } 36 | 37 | declare interface FindOptions { 38 | directories?: Array; 39 | type?: ContentType; 40 | } 41 | 42 | declare interface Platform { 43 | manufacturer?: string; 44 | name?: string; 45 | os?: { 46 | architecture?: number; 47 | family?: string; 48 | version?: string; 49 | }; 50 | ua: string; 51 | version?: string; 52 | } 53 | 54 | declare type InterceptClientRequestCallback = (url: URL) => boolean; 55 | declare type InterceptFileAccessCallback = ( 56 | filePath: string, 57 | mode: 'read' | 'write', 58 | ) => void; 59 | declare type InterceptCreateServerCallback = (origin: string) => void; 60 | 61 | declare interface Watcher { 62 | has(filePath: string): boolean; 63 | add(filePath: string | Array | Set): void; 64 | remove(filePath: string, permanent?: boolean): void; 65 | close(): void; 66 | } 67 | 68 | declare interface RequestContext { 69 | assert: ImportAssertionType; 70 | dynamic: boolean; 71 | filePath?: string; 72 | href: string; 73 | imported: boolean; 74 | type?: ContentType; 75 | } 76 | 77 | declare type ImportAssertionType = 'css' | 'json' | undefined; 78 | -------------------------------------------------------------------------------- /src/utils/base64Url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a string to a URL-safe base64 string 3 | * @param { string } string 4 | * @param { boolean } isBase64 5 | */ 6 | export function toBase64Url(string, isBase64 = false) { 7 | const base64 = isBase64 8 | ? string 9 | : Buffer.from(string, 'utf-8').toString('base64'); 10 | 11 | return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 12 | } 13 | 14 | /** 15 | * Convert a URL-safe base64 string to a string 16 | * @param { string } base64 17 | */ 18 | export function fromBase64Url(base64) { 19 | const segmentLength = base64.length % 4; 20 | 21 | return Buffer.from( 22 | base64 23 | .replace(/_/g, '/') 24 | .replace(/-/g, '+') 25 | .padEnd( 26 | base64.length + (segmentLength === 0 ? 0 : 4 - segmentLength), 27 | '=', 28 | ), 29 | 'base64', 30 | ).toString('utf-8'); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/bootstrap.js: -------------------------------------------------------------------------------- 1 | import config from '../config.js'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | 5 | /** 6 | * Create directory structure: 7 | * .dvlp/ 8 | * - / 9 | * - cached/ 10 | * - bundled/ 11 | */ 12 | export function bootstrap() { 13 | const { bundleDirPath, cacheDirPath, dirPath, versionDirPath } = config; 14 | const bundleDirExists = fs.existsSync(bundleDirPath); 15 | const cacheDirExists = fs.existsSync(cacheDirPath); 16 | const dirExists = fs.existsSync(dirPath); 17 | const subdirExists = fs.existsSync(versionDirPath); 18 | 19 | // New version of .dvlp, so delete existing 20 | if (dirExists && !subdirExists) { 21 | for (const item of fs.readdirSync(dirPath)) { 22 | fs.rmSync(path.resolve(dirPath, item), { force: true, recursive: true }); 23 | } 24 | } 25 | if (!bundleDirExists) { 26 | fs.mkdirSync(bundleDirPath, { recursive: true }); 27 | } 28 | if (!cacheDirExists) { 29 | fs.mkdirSync(cacheDirPath, { recursive: true }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/broken-named-exports.js: -------------------------------------------------------------------------------- 1 | export default { 2 | react: [ 3 | 'Children', 4 | 'Component', 5 | 'Fragment', 6 | 'Profiler', 7 | 'PureComponent', 8 | 'StrictMode', 9 | 'Suspense', 10 | '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED', 11 | 'cloneElement', 12 | 'createContext', 13 | 'createElement', 14 | 'createFactory', 15 | 'createRef', 16 | 'forwardRef', 17 | 'isValidElement', 18 | 'lazy', 19 | 'memo', 20 | 'useCallback', 21 | 'useContext', 22 | 'useDebugValue', 23 | 'useEffect', 24 | 'useImperativeHandle', 25 | 'useLayoutEffect', 26 | 'useMemo', 27 | 'useReducer', 28 | 'useRef', 29 | 'useState', 30 | 'version', 31 | ], 32 | 'react-dom': [ 33 | '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED', 34 | 'createPortal', 35 | 'findDOMNode', 36 | 'flushSync', 37 | 'hydrate', 38 | 'render', 39 | 'unmountComponentAtNode', 40 | 'unstable_batchedUpdates', 41 | 'unstable_createPortal', 42 | 'unstable_renderSubtreeIntoContainer', 43 | 'version', 44 | ], 45 | 'react-is': ['isContextConsumer', 'isValidElementType'], 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/bundling.js: -------------------------------------------------------------------------------- 1 | import { 2 | existsSync, 3 | readdirSync, 4 | readFileSync, 5 | unlinkSync, 6 | writeFileSync, 7 | } from 'node:fs'; 8 | import config from '../config.js'; 9 | import { getPackageForDir } from '../resolver/index.js'; 10 | import { isJsFilePath } from './is.js'; 11 | import path from 'node:path'; 12 | 13 | const { bundleDirMetaPath } = config; 14 | /** @type { Record } */ 15 | let meta = {}; 16 | 17 | if (existsSync(bundleDirMetaPath)) { 18 | meta = JSON.parse(readFileSync(bundleDirMetaPath, 'utf-8')); 19 | } 20 | 21 | /** 22 | * Get path to bundle from 23 | * 24 | * @param { string } specifier 25 | * @param { string } sourcePath 26 | */ 27 | export function getBundlePath(specifier, sourcePath) { 28 | const pkg = getPackageForDir(path.dirname(sourcePath)); 29 | const bundleName = `${encodeBundleSpecifier(specifier)}-${ 30 | pkg ? pkg.version : '' 31 | }.js`; 32 | const bundlePath = path.join(config.bundleDirName, bundleName); 33 | 34 | meta[bundleName] = sourcePath; 35 | 36 | writeFileSync(bundleDirMetaPath, JSON.stringify(meta)); 37 | 38 | return bundlePath; 39 | } 40 | 41 | /** 42 | * Get original source path from "bundlePath" 43 | * 44 | * @param { string } bundlePath 45 | * @returns [specifier: string, sourcePath: string] 46 | */ 47 | export function getBundleSourcePath(bundlePath) { 48 | const bundleName = path.basename(bundlePath); 49 | const specifier = decodeBundleSpecifier(bundleName.split('-')[0]); 50 | const sourcePath = meta[bundleName]; 51 | 52 | return [specifier, sourcePath]; 53 | } 54 | 55 | /** 56 | * Clear disk cache 57 | */ 58 | export function cleanBundledFiles() { 59 | if (existsSync(config.bundleDirPath)) { 60 | for (const filePath of readdirSync(config.bundleDirPath).filter( 61 | isJsFilePath, 62 | )) { 63 | try { 64 | unlinkSync(path.join(config.bundleDirPath, filePath)); 65 | } catch { 66 | // ignore 67 | } 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Encode "id" 74 | * 75 | * @param { string } id 76 | */ 77 | function encodeBundleSpecifier(id) { 78 | return id.replace(/\//g, '__'); 79 | } 80 | 81 | /** 82 | * Decode "id" 83 | * 84 | * @param { string } id 85 | */ 86 | function decodeBundleSpecifier(id) { 87 | return id.replace(/__/g, '/'); 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/expand-path.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import glob from 'fast-glob'; 3 | import path from 'node:path'; 4 | 5 | const RE_GLOB = /[*[{]/; 6 | const RE_SEPARATOR = /[,;]\s?|\s/g; 7 | 8 | /** 9 | * Expand "filePath" into multiple filePaths 10 | * Handles globs and/or separators 11 | * 12 | * @param { string | Array } filePath 13 | * @returns { Array } 14 | */ 15 | export function expandPath(filePath) { 16 | if (!filePath) { 17 | return []; 18 | } 19 | 20 | if (typeof filePath === 'string' && fs.existsSync(path.resolve(filePath))) { 21 | return [filePath]; 22 | } 23 | 24 | if (Array.isArray(filePath)) { 25 | return filePath.reduce((/** @type { Array } */ filePaths, fp) => { 26 | if (fp) { 27 | filePaths.push(...expandPath(fp)); 28 | } 29 | return filePaths; 30 | }, []); 31 | } 32 | 33 | RE_SEPARATOR.lastIndex = 0; 34 | if (RE_SEPARATOR.test(filePath)) { 35 | filePath = filePath.split(RE_SEPARATOR); 36 | } 37 | if (!Array.isArray(filePath)) { 38 | filePath = [filePath]; 39 | } 40 | 41 | return filePath.reduce((/** @type { Array } */ filePaths, fp) => { 42 | if (RE_GLOB.test(fp)) { 43 | filePaths.push(...glob.sync(fp)); 44 | } else { 45 | filePaths.push(fp); 46 | } 47 | return filePaths; 48 | }, []); 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/intercept-create-server.js: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import http2 from 'node:http2'; 3 | import https from 'node:https'; 4 | import { syncBuiltinESMExports } from 'node:module'; 5 | import util from 'node:util'; 6 | 7 | /** @type { Set } */ 8 | const createServerListeners = new Set(); 9 | const originalHttpCreateServer = http.createServer; 10 | const originalHttp2CreateSecureServer = http2.createSecureServer; 11 | const originalHttpsCreateServer = https.createServer; 12 | 13 | /** 14 | * Listen for created servers 15 | * 16 | * @param { number } reservedPort 17 | * @param { InterceptCreateServerCallback } fn 18 | * @returns { () => void } 19 | */ 20 | export function interceptCreateServer(reservedPort, fn) { 21 | createServerListeners.add(fn); 22 | initInterceptCreateServer(reservedPort); 23 | return restoreCreateServer.bind(null, fn); 24 | } 25 | 26 | /** 27 | * Initialise `http.createServer` proxy 28 | * 29 | * @param { number } reservedPort 30 | */ 31 | function initInterceptCreateServer(reservedPort) { 32 | if (!util.types.isProxy(http.createServer)) { 33 | for (const [lib, method] of [ 34 | [http, 'createServer'], 35 | [http2, 'createSecureServer'], 36 | [https, 'createServer'], 37 | ]) { 38 | // @ts-expect-error - patch 39 | lib[method] = new Proxy(lib[method], { 40 | apply(target, ctx, args) { 41 | /** @type { import('http').Server } */ 42 | const server = Reflect.apply(target, ctx, args); 43 | 44 | server.on('error', (err) => { 45 | throw err; 46 | }); 47 | server.once('listening', () => { 48 | const protocol = lib === http ? 'http' : 'https'; 49 | const { port } = /** @type { import('net').AddressInfo } */ ( 50 | server.address() 51 | ); 52 | const origin = `${protocol}://localhost:${port}`; 53 | 54 | for (const listener of createServerListeners) { 55 | listener(origin); 56 | } 57 | }); 58 | server.listen = new Proxy(server.listen, { 59 | // Randomize port if same as reserved 60 | apply(target, ctx, args) { 61 | // listen(options) 62 | if (typeof args[0] === 'object') { 63 | if (args[0].port === reservedPort) { 64 | args[0].port = 0; 65 | } 66 | } 67 | // listen(port[, host]) 68 | else if (typeof args[0] === 'number') { 69 | if (args[0] === reservedPort) { 70 | args[0] = 0; 71 | } 72 | } 73 | // listen('localhost:port') 74 | else if (typeof args[0] === 'string') { 75 | const [, port] = args[0].split(':'); 76 | if (Number(port) === reservedPort) { 77 | args[0] = 0; 78 | } 79 | } 80 | 81 | return Reflect.apply(target, ctx, args); 82 | }, 83 | }); 84 | 85 | return server; 86 | }, 87 | }); 88 | } 89 | 90 | syncBuiltinESMExports(); 91 | } 92 | } 93 | 94 | /** 95 | * Restore unproxied create server behaviour 96 | * 97 | * @param { InterceptCreateServerCallback } fn 98 | */ 99 | function restoreCreateServer(fn) { 100 | createServerListeners.delete(fn); 101 | if (!createServerListeners.size) { 102 | http.createServer = originalHttpCreateServer; 103 | http2.createSecureServer = originalHttp2CreateSecureServer; 104 | https.createServer = originalHttpsCreateServer; 105 | syncBuiltinESMExports(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/intercept-file-access.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { syncBuiltinESMExports } from 'node:module'; 3 | import util from 'node:util'; 4 | 5 | /** @type { Set } */ 6 | const fileAccessListeners = new Set(); 7 | const originalReadStreamRead = fs.ReadStream.prototype._read; 8 | const originalReadFile = fs.readFile; 9 | const originalReadFileSync = fs.readFileSync; 10 | const originalWriteFile = fs.writeFile; 11 | const originalWriteFileSync = fs.writeFileSync; 12 | 13 | // Early init to ensure that 3rd-party libraries use proxied versions 14 | initInterceptFileAccess(); 15 | 16 | /** 17 | * Listen for file system reads and report 18 | * 19 | * @param { InterceptFileAccessCallback } fn 20 | * @returns { () => void } 21 | */ 22 | export function interceptFileAccess(fn) { 23 | initInterceptFileAccess(); 24 | fileAccessListeners.add(fn); 25 | return restoreFileAccess.bind(null, fn); 26 | } 27 | 28 | /** 29 | * Initialise `fileRead` proxy 30 | */ 31 | function initInterceptFileAccess() { 32 | if (!util.types.isProxy(fs.readFile)) { 33 | // Proxy ReadStream private method to work around patching by graceful-fs 34 | const ReadStream = fs.ReadStream.prototype; 35 | 36 | ReadStream._read = new Proxy(ReadStream._read, { 37 | apply(target, ctx, args) { 38 | notifyListeners(fileAccessListeners, String(ctx.path), 'read'); 39 | return Reflect.apply(target, ctx, args); 40 | }, 41 | }); 42 | 43 | for (const method of ['readFile', 'readFileSync']) { 44 | // @ts-expect-error - patch 45 | fs[method] = new Proxy(fs[method], { 46 | apply(target, ctx, args) { 47 | notifyListeners(fileAccessListeners, String(args[0]), 'read'); 48 | return Reflect.apply(target, ctx, args); 49 | }, 50 | }); 51 | } 52 | for (const method of ['writeFile', 'writeFileSync']) { 53 | // @ts-expect-error - patch 54 | fs[method] = new Proxy(fs[method], { 55 | apply(target, ctx, args) { 56 | notifyListeners(fileAccessListeners, String(args[0]), 'write'); 57 | return Reflect.apply(target, ctx, args); 58 | }, 59 | }); 60 | } 61 | 62 | syncBuiltinESMExports(); 63 | } 64 | } 65 | 66 | /** 67 | * Restore unproxied file reading behaviour 68 | * 69 | * @param { InterceptFileAccessCallback } fn 70 | */ 71 | function restoreFileAccess(fn) { 72 | fileAccessListeners.delete(fn); 73 | if (!fileAccessListeners.size) { 74 | fs.ReadStream.prototype._read = originalReadStreamRead; 75 | fs.readFile = originalReadFile; 76 | fs.readFileSync = originalReadFileSync; 77 | fs.writeFile = originalWriteFile; 78 | fs.writeFileSync = originalWriteFileSync; 79 | syncBuiltinESMExports(); 80 | } 81 | } 82 | 83 | /** 84 | * Notify 'listeners' with 'args' 85 | * 86 | * @param { Set } listeners 87 | * @param { string } filePath 88 | * @param { 'read' | 'write' } mode 89 | */ 90 | function notifyListeners(listeners, filePath, mode) { 91 | for (const listener of listeners) { 92 | listener(filePath, mode); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/intercept-in-process.js: -------------------------------------------------------------------------------- 1 | import { getRepoPath } from './file.js'; 2 | import { interceptClientRequest } from './intercept-client-request.js'; 3 | import { interceptCreateServer } from './intercept-create-server.js'; 4 | import { interceptFileAccess } from './intercept-file-access.js'; 5 | import { isEqualSearchParams } from './url.js'; 6 | 7 | /** 8 | * @param { ApplicationProcessWorkerData | ElectronProcessWorkerData } workerData 9 | */ 10 | export function interceptInProcess(workerData) { 11 | const hostUrl = new URL(workerData.hostOrigin); 12 | const mocks = workerData.serializedMocks?.map((mockData) => { 13 | return { 14 | ...mockData, 15 | originRegex: new RegExp(mockData.originRegex), 16 | pathRegex: new RegExp(mockData.pathRegex), 17 | search: new URLSearchParams(mockData.search), 18 | }; 19 | }); 20 | 21 | // Capture application/renderer server ports 22 | interceptCreateServer( 23 | // Default port numbers are ignored when parsed in URL 24 | Number(hostUrl.port || (hostUrl.protocol === 'http:' ? 80 : 443)), 25 | (origin) => { 26 | workerData.postMessage({ type: 'listening', origin }); 27 | }, 28 | ); 29 | 30 | // Redirect mocked request to host 31 | interceptClientRequest((url) => { 32 | if (mocks) { 33 | for (const mock of mocks) { 34 | if ( 35 | !mock.originRegex.test(url.origin) || 36 | (!mock.ignoreSearch && 37 | mock.search && 38 | !isEqualSearchParams(url.searchParams, mock.search)) 39 | ) { 40 | continue; 41 | } 42 | 43 | if (mock.pathRegex.exec(url.pathname) != null) { 44 | const { href } = url; 45 | // Reroute back to host server 46 | url.protocol = 'http:'; 47 | url.host = hostUrl.host; 48 | url.search = `?dvlpmock=${encodeURIComponent(href)}`; 49 | return true; 50 | } 51 | } 52 | } 53 | 54 | return false; 55 | }); 56 | 57 | // Notify to watch project files 58 | interceptFileAccess((filePath, mode) => { 59 | if (filePath.startsWith(getRepoPath())) { 60 | workerData.postMessage({ type: 'watch', filePath, mode }); 61 | } 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const WARN_BARE_IMPORT = `${chalk.yellow('⚠️')} re-writing bare import`; 4 | export const WARN_MISSING_EXTENSION = `${chalk.yellow( 5 | '⚠️', 6 | )} adding missing file extension for`; 7 | export const WARN_PACKAGE_INDEX = `${chalk.yellow( 8 | '⚠️', 9 | )} adding missing package "index.js" for`; 10 | export const WARN_CERTIFICATE_EXPIRY = `${chalk.yellow( 11 | '⚠️', 12 | )} ssl certificate will expire soon!`; 13 | 14 | const SEG_LENGTH = 80; 15 | 16 | const seenWarnings = new Set(); 17 | let level = 1; 18 | 19 | export default { 20 | /** 21 | * Set silent state 22 | * 23 | * @param { boolean } value 24 | */ 25 | set silent(value) { 26 | level = 0; 27 | }, 28 | /** 29 | * Set silent state 30 | * 31 | * @param { boolean } value 32 | */ 33 | set verbose(value) { 34 | level = 2; 35 | }, 36 | }; 37 | 38 | /** 39 | * Log if verbose 40 | * 41 | * @param { string } msg 42 | */ 43 | export function info(msg) { 44 | if (level > 1) { 45 | console.log(truncate(' ' + msg.replace(/\\/g, '/'))); 46 | } 47 | } 48 | 49 | /** 50 | * Log if not silent 51 | * 52 | * @param { string } msg 53 | */ 54 | export function noisyInfo(msg) { 55 | if (level > 0) { 56 | console.log(truncate(' ' + msg.replace(/\\/g, '/'))); 57 | } 58 | } 59 | 60 | /** 61 | * Warn if verbose 62 | * 63 | * @param { ...unknown } args 64 | */ 65 | export function warn(...args) { 66 | if (level > 1) { 67 | const warning = args.join(' '); 68 | 69 | // Only warn one time 70 | if (seenWarnings.has(warning)) { 71 | return; 72 | } 73 | seenWarnings.add(warning); 74 | 75 | console.warn(warning); 76 | } 77 | } 78 | 79 | /** 80 | * Warn if not silent 81 | * 82 | * @param { ...unknown } args 83 | */ 84 | export function noisyWarn(...args) { 85 | if (level > 0) { 86 | const initialLevel = level; 87 | level = 2; 88 | warn(...args); 89 | level = initialLevel; 90 | } 91 | } 92 | 93 | /** 94 | * Error 95 | * 96 | * @param { ...unknown } args 97 | */ 98 | export function error(...args) { 99 | if (level > 0) { 100 | console.error('\n', chalk.red.inverse(' error '), ...args, '\n'); 101 | } 102 | } 103 | 104 | /** 105 | * Fatal error 106 | * 107 | * @param { ...unknown } args 108 | */ 109 | export function fatal(...args) { 110 | if (level > 0) { 111 | console.error('\n', chalk.red.inverse(' fatal error '), ...args, '\n'); 112 | } 113 | } 114 | 115 | /** 116 | * Truncate 'string' 117 | * 118 | * @param { string } string 119 | * @returns { string } 120 | */ 121 | function truncate(string) { 122 | if (string.length > SEG_LENGTH * 1.5 + 3) { 123 | return string.slice(0, SEG_LENGTH) + '...' + string.slice(-SEG_LENGTH / 2); 124 | } 125 | 126 | return string; 127 | } 128 | -------------------------------------------------------------------------------- /src/utils/metrics.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import { getProjectPath } from './file.js'; 4 | import { performance } from 'node:perf_hooks'; 5 | 6 | const EVENT_NAMES = { 7 | bundle: 'bundle file', 8 | csp: 'inject CSP header', 9 | imports: 'rewrite imports', 10 | mock: 'mock response', 11 | response: 'response', 12 | scripts: 'inject HTML scripts', 13 | transform: 'transform file', 14 | }; 15 | 16 | const debug = Debug('dvlp:metrics'); 17 | 18 | export class Metrics { 19 | /** 20 | * Constructor 21 | * 22 | * @param { Res } res 23 | */ 24 | constructor(res) { 25 | /** @type { Map } */ 26 | this.events = new Map(); 27 | this.recordEvent(EVENT_NAMES.response); 28 | res.once('finish', () => { 29 | this.recordEvent(EVENT_NAMES.response); 30 | if (debug.enabled) { 31 | let results = ''; 32 | for (const [name, times] of this.events) { 33 | if (times[1] > 0) { 34 | results += ` ${name}: ${this.getEvent(name, true)}\n`; 35 | } 36 | } 37 | debug(getProjectPath(res.url)); 38 | console.log(results); 39 | } 40 | }); 41 | } 42 | 43 | /** 44 | * Register new event with "name", 45 | * or complete existing event if already registered. 46 | * 47 | * @param { string } name 48 | */ 49 | recordEvent(name) { 50 | if (!this.events.has(name)) { 51 | this.events.set(name, [performance.now(), 0]); 52 | } else { 53 | // @ts-expect-error - non-null 54 | this.events.get(name)[1] = performance.now(); 55 | } 56 | } 57 | 58 | /** 59 | * Retrieve results for event with "name" 60 | * 61 | * @param { string } name 62 | * @param { boolean } [formatted] 63 | * @returns { string | number } 64 | */ 65 | getEvent(name, formatted) { 66 | const times = this.events.get(name); 67 | const duration = times && times[1] > 0 ? msDiff(times) : 0; 68 | 69 | return formatted ? format(duration) : duration; 70 | } 71 | } 72 | 73 | Metrics.EVENT_NAMES = EVENT_NAMES; 74 | 75 | /** 76 | * Retrieve rounded difference 77 | * @param { [number, number] } times 78 | * @returns { number } 79 | */ 80 | export function msDiff(times) { 81 | return Math.ceil((times[1] - times[0]) * 100) / 100; 82 | } 83 | 84 | /** 85 | * Format 'duration' 86 | * 87 | * @param { number } duration - ms 88 | * @returns { string } 89 | */ 90 | export function format(duration) { 91 | const colour = duration > 10 ? (duration > 100 ? 'red' : 'yellow') : 'green'; 92 | let formatted = 93 | duration < 1000 94 | ? `${duration}ms` 95 | : `${Math.floor((duration / 1000) * 100) / 100}s`; 96 | 97 | formatted = formatted.padStart(7, ' '); 98 | 99 | return chalk[colour](formatted); 100 | } 101 | -------------------------------------------------------------------------------- /src/utils/mime.js: -------------------------------------------------------------------------------- 1 | import config from '../config.js'; 2 | import path from 'node:path'; 3 | 4 | const TYPES = { 5 | 'text/css': config.extensionsByType.css, 6 | 'text/html': config.extensionsByType.html, 7 | 'application/javascript': config.extensionsByType.js.filter( 8 | (ext) => ext !== '.json', 9 | ), 10 | 'application/json': ['.json', '.json5'], 11 | 'image/gif': ['.gif'], 12 | 'image/jpeg': ['.jpeg', '.jpg', '.jpe'], 13 | 'image/png': ['.png'], 14 | 'image/svg+xml': ['.svg', '.svgz'], 15 | 'image/webp': ['.webp'], 16 | 'font/otf': ['.otf'], 17 | 'font/ttf': ['.ttf'], 18 | 'font/woff': ['.woff'], 19 | 'font/woff2': ['.woff2'], 20 | 'video/mp4': ['.mp4'], 21 | }; 22 | 23 | /** 24 | * Retrieve the mime type for 'filePath' 25 | * 26 | * @param { string } filePath 27 | */ 28 | export function getType(filePath) { 29 | const ext = path.extname(filePath); 30 | 31 | for (const [type, extensions] of Object.entries(TYPES)) { 32 | if (extensions.includes(ext)) { 33 | return type; 34 | } 35 | } 36 | 37 | return 'application/octet-stream'; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/module.js: -------------------------------------------------------------------------------- 1 | import { isJsFilePath, isNodeModuleFilePath } from './is.js'; 2 | import esbuild from 'esbuild'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | /** 6 | * Retrieve all dependencies for "filePath" 7 | * 8 | * @param { string } filePath 9 | * @param { 'browser' | 'node' } platform 10 | */ 11 | export async function getDependencies(filePath, platform) { 12 | if (filePath.startsWith('file://')) { 13 | filePath = fileURLToPath(filePath); 14 | } 15 | 16 | /** @type { Set } */ 17 | const dependencies = new Set([filePath]); 18 | 19 | if (isJsFilePath(filePath)) { 20 | try { 21 | await esbuild.build({ 22 | bundle: true, 23 | define: { 'process.env.NODE_ENV': '"development"' }, 24 | entryPoints: [filePath], 25 | format: 'esm', 26 | logLevel: 'silent', 27 | minify: true, 28 | platform, 29 | splitting: false, 30 | target: 'esnext', 31 | treeShaking: false, 32 | write: false, 33 | plugins: [ 34 | { 35 | name: 'deps', 36 | setup(build) { 37 | // @ts-expect-error - works 38 | build.onLoad({ filter: /.*/ }, (args) => { 39 | if (!isNodeModuleFilePath(args.path)) { 40 | dependencies.add(args.path); 41 | } 42 | }); 43 | }, 44 | }, 45 | ], 46 | }); 47 | } catch { 48 | // Ignore 49 | } 50 | } 51 | 52 | return dependencies; 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/platform.js: -------------------------------------------------------------------------------- 1 | import config from '../config.js'; 2 | import platform from 'platform'; 3 | 4 | const ESBUILD_BROWSER_ENGINES = ['chrome', 'edge', 'firefox', 'ios', 'safari']; 5 | 6 | /** 7 | * Parse platform information from User-Agent 8 | * 9 | * @param { string } [userAgent] 10 | * @returns { Platform } 11 | */ 12 | export function parseUserAgent(userAgent) { 13 | const dvlpUA = `dvlp/${config.version} (+https://github.com/popeindustries/dvlp)`; 14 | 15 | if (!userAgent) { 16 | return { 17 | manufacturer: 'Popeindustries', 18 | name: 'dvlp', 19 | ua: dvlpUA, 20 | version: config.version, 21 | }; 22 | } 23 | 24 | const { 25 | manufacturer, 26 | name, 27 | os, 28 | ua = dvlpUA, 29 | version, 30 | } = platform.parse( 31 | // Some platforms (Tizen smart-tv) are missing browser name, so assume Chrome 32 | userAgent.replace(/(Gecko\) )([0-9])/, '$1Chrome/$2'), 33 | ); 34 | 35 | return { 36 | manufacturer, 37 | name: name === null ? undefined : name, 38 | os, 39 | ua, 40 | version: version ? version.split('.')[0] : undefined, 41 | }; 42 | } 43 | 44 | /** 45 | * Parse valid esbuild transform target from "platform" instance 46 | * 47 | * @param { Platform } platform 48 | * @returns { string } 49 | */ 50 | export function parseEsbuildTarget(platform) { 51 | const { name = '', os: { family } = {}, version } = platform; 52 | const engine = family === 'iOS' ? 'ios' : name.split(' ')[0].toLowerCase(); 53 | 54 | if ( 55 | !engine || 56 | engine === 'dvlp' || 57 | !version || 58 | !ESBUILD_BROWSER_ENGINES.includes(engine) 59 | ) { 60 | return 'es2020'; 61 | } 62 | 63 | return `${engine}${version}`; 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/regexp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { string } string 3 | */ 4 | export function escapeRegExp(string) { 5 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/request-contexts.js: -------------------------------------------------------------------------------- 1 | import { find, getTypeFromPath, getTypeFromRequest } from './file.js'; 2 | import fs from 'node:fs'; 3 | 4 | /** @type { Map } */ 5 | const contextByHref = new Map(); 6 | 7 | /** 8 | * Retrieve context for "req". 9 | * Creates new context if not already cached. 10 | * 11 | * @param { Req } req 12 | */ 13 | export function getContextForReq(req) { 14 | // Ignore search params 15 | const url = new URL(req.url, 'http://localhost'); 16 | const cached = contextByHref.get(url.pathname); 17 | const type = getTypeFromRequest(req); 18 | 19 | if ( 20 | cached && 21 | cached.type === type && 22 | cached.filePath !== undefined && 23 | fs.existsSync(cached.filePath) 24 | ) { 25 | return cached; 26 | } 27 | 28 | const filePath = find(req, { type }); 29 | const context = { 30 | assert: undefined, 31 | dynamic: false, 32 | filePath, 33 | href: req.url, 34 | imported: false, 35 | type: type ?? getTypeFromPath(filePath), 36 | }; 37 | 38 | contextByHref.set(url.pathname, context); 39 | 40 | return context; 41 | } 42 | 43 | /** 44 | * Retrieve existing context for "filePath" 45 | * 46 | * @param { string } filePath 47 | */ 48 | export function getContextForFilePath(filePath) { 49 | for (const context of contextByHref.values()) { 50 | if (context.filePath === filePath) { 51 | return context; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * Create new context 58 | * 59 | * @param { string } href 60 | * @param { ImportAssertionType } assert 61 | * @param { boolean } dynamic 62 | * @param { string } filePath 63 | * @param { boolean } imported 64 | * @param { ContentType } type 65 | */ 66 | export function createContext(href, assert, dynamic, filePath, imported, type) { 67 | contextByHref.set(href, { 68 | assert, 69 | dynamic, 70 | filePath, 71 | href, 72 | imported, 73 | type, 74 | }); 75 | } 76 | 77 | /** 78 | * Clear cached contexts 79 | */ 80 | export function clearContexts() { 81 | contextByHref.clear(); 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { import('node:http').ClientRequest } ClientRequest 3 | * @typedef { import('node:http').IncomingHttpHeaders } IncomingHttpHeaders 4 | */ 5 | 6 | import { request } from 'node:http'; 7 | import { request as secureRequest } from 'node:https'; 8 | 9 | const FORBIDDEN_REQUEST_HEADERS = [ 10 | 'connection', 11 | 'content-length', 12 | 'host', 13 | 'sec-fetch-mode', 14 | ]; 15 | const FORBIDDEN_RESPONSE_HEADERS = [ 16 | 'connection', 17 | 'content-encoding', 18 | 'content-length', 19 | 'content-security-policy', 20 | 'keep-alive', 21 | 'strict-transport-security', 22 | 'transfer-encoding', 23 | ]; 24 | 25 | /** 26 | * Forward request to `origin`. 27 | * 28 | * @param { Set } origins 29 | * @param { Req } req 30 | * @param { Res } res 31 | */ 32 | export async function forwardRequest(origins, req, res) { 33 | for (const origin of origins) { 34 | const url = new URL(origin); 35 | const requestOptions = { 36 | headers: copyRequestHeaders(req.headers, {}), 37 | method: req.method, 38 | host: url.hostname, 39 | path: req.url, 40 | port: url.port, 41 | protocol: url.protocol, 42 | rejectUnauthorized: false, 43 | }; 44 | const requestFn = url.protocol === 'https:' ? secureRequest : request; 45 | const fwdRequest = requestFn(requestOptions); 46 | 47 | req.pipe(fwdRequest); 48 | 49 | try { 50 | const fwdResponse = await getForwardResponse(fwdRequest); 51 | const statusCode = /** @type { number } */ (fwdResponse.statusCode); 52 | 53 | if (statusCode !== 404) { 54 | res.writeHead(statusCode, copyResponseHeaders(fwdResponse.headers, {})); 55 | fwdResponse.pipe(res); 56 | return; 57 | } 58 | } catch { 59 | // Continue to next origin 60 | } 61 | } 62 | 63 | if (!res.headersSent) { 64 | res.writeHead(404); 65 | res.end(); 66 | } 67 | } 68 | 69 | /** 70 | * @param { ClientRequest } fwdRequest 71 | * @returns { Promise } 72 | */ 73 | function getForwardResponse(fwdRequest) { 74 | return new Promise((resolve, reject) => { 75 | fwdRequest.on('response', (originResponse) => { 76 | resolve(originResponse); 77 | }); 78 | 79 | fwdRequest.on('error', (err) => { 80 | reject(err); 81 | }); 82 | }); 83 | } 84 | 85 | /** 86 | * @param { IncomingHttpHeaders } from 87 | * @param { Record } to 88 | */ 89 | function copyRequestHeaders(from, to) { 90 | for (const [header, value] of Object.entries(from)) { 91 | if ( 92 | !header.startsWith(':') && 93 | !FORBIDDEN_REQUEST_HEADERS.includes(header) 94 | ) { 95 | to[header] = /** @type { string } */ (value); 96 | } 97 | } 98 | 99 | return to; 100 | } 101 | 102 | /** 103 | * @param { IncomingHttpHeaders } from 104 | * @param { Record } to 105 | */ 106 | function copyResponseHeaders(from, to) { 107 | for (const [header, value] of Object.entries(from)) { 108 | if (!FORBIDDEN_RESPONSE_HEADERS.includes(header)) { 109 | to[header] = /** @type { string } */ (value); 110 | } 111 | } 112 | 113 | return to; 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/scripts.js: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto'; 2 | 3 | /** 4 | * Retrieve process.env polyfill 5 | * 6 | * @returns { string } 7 | */ 8 | export function getProcessEnvString() { 9 | return `window.process=window.process||{env:{}};window.process.env.NODE_ENV="${ 10 | process.env.NODE_ENV || 'development' 11 | }";`; 12 | } 13 | 14 | /** 15 | * Retrieve DVLP global 16 | * 17 | * @returns { string } 18 | */ 19 | export function getDvlpGlobalString() { 20 | return 'window.DVLP=true;'; 21 | } 22 | 23 | /** 24 | * Retrieve patched "adoptedStyleSheets". 25 | * This is used to capture all adoptedStyleSheet asignments to enable css hot-reload 26 | * 27 | * @returns { string } 28 | */ 29 | export function getPatchedAdoptedStyleSheets() { 30 | return `window.__adoptedStyleSheets__ = { sheets: [], add(sheets) { this.sheets.push(...sheets); } }; 31 | for (const proto of [Document.prototype, ShadowRoot.prototype]) { 32 | const old = Object.getOwnPropertyDescriptor(proto, 'adoptedStyleSheets'); 33 | Object.defineProperty(proto, 'adoptedStyleSheets', { 34 | set: function (sheets) { 35 | window.__adoptedStyleSheets__.add(sheets); 36 | return old.set.call(this, sheets); 37 | }, 38 | }); 39 | }`; 40 | } 41 | 42 | /** 43 | * Concatenate multiple "scripts" into a single string 44 | * 45 | * @param { Array } scripts 46 | * @return { string } 47 | */ 48 | export function concatScripts(scripts) { 49 | return scripts.filter((script) => !!script).join('\n'); 50 | } 51 | 52 | /** 53 | * Retrieve sha256 hash of "script" 54 | * 55 | * @param { string } script 56 | * @returns { string } 57 | */ 58 | export function hashScript(script) { 59 | return crypto.createHash('sha256').update(script).digest('base64'); 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/send.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { getType } from './mime.js'; 3 | 4 | /** 5 | * Handle file request 6 | * 7 | * @param { string } filePath 8 | * @param { Res } res 9 | */ 10 | export function send(filePath, res) { 11 | if (res.headersSent) { 12 | return; 13 | } 14 | 15 | if (res.getHeader('Content-Type') === undefined) { 16 | const type = getType(filePath); 17 | res.setHeader('Content-Type', type); 18 | } 19 | 20 | try { 21 | const stat = fs.statSync(filePath); 22 | 23 | if (stat.isFile()) { 24 | res.setHeader('Content-Length', stat.size); 25 | 26 | const stream = fs.createReadStream(filePath); 27 | 28 | stream.on( 29 | 'error', 30 | /** @param { Error } error */ 31 | (error) => { 32 | // @ts-expect-error - it exists 33 | if (error.code === 'ENOENT') { 34 | res.writeHead(404); 35 | res.end('Not Found'); 36 | } else { 37 | res.writeHead(500); 38 | res.end('Internal Server Error'); 39 | } 40 | }, 41 | ); 42 | 43 | stream.pipe(res); 44 | } 45 | } catch { 46 | res.writeHead(404); 47 | res.end('Not Found'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Function} fn 3 | * @param {number} limit 4 | */ 5 | export function throttle(fn, limit) { 6 | let throttled = false; 7 | 8 | /** 9 | * @param {any[]} args 10 | */ 11 | return function (...args) { 12 | if (!throttled) { 13 | throttled = true; 14 | setTimeout(() => { 15 | throttled = false; 16 | fn(...args); 17 | }, limit); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/url.js: -------------------------------------------------------------------------------- 1 | import config from '../config.js'; 2 | 3 | const RE_WEB_SOCKET = /wss?:/; 4 | 5 | /** 6 | * Determine if 'url' is a WebSocket 7 | * 8 | * @param { URL } url 9 | * @returns { boolean } 10 | */ 11 | export function isWebSocketUrl(url) { 12 | return RE_WEB_SOCKET.test(url.protocol); 13 | } 14 | 15 | /** 16 | * Retrieve URL instance from 'req' 17 | * 18 | * @param { string | { url: string } | URL } req 19 | * @returns { URL } 20 | */ 21 | export function getUrl(req) { 22 | if (!(req instanceof URL)) { 23 | req = new URL( 24 | typeof req === 'string' ? decodeURIComponent(req) : req.url, 25 | `http://localhost:${config.activePort}`, 26 | ); 27 | } 28 | // Map loopback address to localhost 29 | if (req.hostname === '127.0.0.1') { 30 | req.hostname = 'localhost'; 31 | } 32 | if (req.pathname.endsWith('/')) { 33 | req.pathname = req.pathname.slice(0, -1); 34 | } 35 | 36 | return req; 37 | } 38 | 39 | /** 40 | * Retrieve key for 'url' 41 | * 42 | * @param { URL } url 43 | * @returns { string } 44 | * @private 45 | */ 46 | export function getUrlCacheKey(url) { 47 | // Map loopback address to localhost 48 | const host = url.host === '127.0.0.1' ? 'localhost' : url.host; 49 | let key = `${host}${url.pathname}`; 50 | 51 | if (key.endsWith('/')) { 52 | key = key.slice(0, -1); 53 | } 54 | 55 | return key; 56 | } 57 | 58 | /** 59 | * Convert file path to valid url 60 | * Handles platform differences 61 | * 62 | * @param { string } filePath 63 | * @returns { string } 64 | */ 65 | export function filePathToUrlPathname(filePath) { 66 | return encodeURI( 67 | filePath 68 | .replace(/^(?:file:\/\/)|(?:[a-zA-Z]:[\\/])/, '/') 69 | .replace(/\\/g, '/'), 70 | ); 71 | } 72 | 73 | /** 74 | * Determine if search params are equal 75 | * 76 | * @param { URLSearchParams } params1 77 | * @param { URLSearchParams } params2 78 | * @returns { boolean } 79 | */ 80 | export function isEqualSearchParams(params1, params2) { 81 | const keys1 = Array.from(params1.keys()); 82 | const keys2 = Array.from(params2.keys()); 83 | 84 | if (keys1.length !== keys2.length) { 85 | return false; 86 | } 87 | 88 | for (const key of keys1) { 89 | const values1 = params1.getAll(key); 90 | const values2 = params2.getAll(key); 91 | 92 | if (values1.length !== values2.length) { 93 | return false; 94 | } 95 | 96 | for (const value of values1) { 97 | if (!values2.includes(value)) { 98 | return false; 99 | } 100 | } 101 | } 102 | 103 | return true; 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/watch.js: -------------------------------------------------------------------------------- 1 | import config from '../config.js'; 2 | import Debug from 'debug'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { FSWatcher } from 'chokidar'; 5 | import { getProjectPath } from './file.js'; 6 | import { isNodeModuleFilePath } from './is.js'; 7 | import os from 'node:os'; 8 | import path from 'node:path'; 9 | 10 | const CHANGE_DELAY = 250; 11 | const IGNORE_CHANGE_WINDOW = 750; 12 | 13 | const debug = Debug('dvlp:watch'); 14 | const tmpdir = os.tmpdir(); 15 | 16 | /** 17 | * Instantiate a file watcher and begin watching for changes 18 | * 19 | * @param { (callback: string) => void } fn 20 | * @returns { Watcher } 21 | */ 22 | export function watch(fn) { 23 | /** @type { Set } */ 24 | const banned = new Set(); 25 | /** @type {Set} */ 26 | const changingFiles = new Set(); 27 | /** @type { Set } */ 28 | const files = new Set(); 29 | const watcher = new FSWatcher({ 30 | ignoreInitial: true, 31 | persistent: true, 32 | }); 33 | let changePending = false; 34 | 35 | watcher.on('unlink', (filePath) => { 36 | debug(`unwatching file "${getProjectPath(filePath)}"`); 37 | watcher.unwatch(filePath); 38 | files.delete(path.resolve(filePath)); 39 | }); 40 | watcher.on('change', (filePath) => { 41 | if (!changePending && !changingFiles.has(filePath)) { 42 | changePending = true; 43 | changingFiles.add(filePath); 44 | 45 | // Delay to allow time for files to be unwatched when file write intercepted in secondary process 46 | setTimeout(() => { 47 | if (files.has(filePath)) { 48 | // Delay to ignore duplicate changes to same file 49 | setTimeout(() => { 50 | changingFiles.delete(filePath); 51 | }, IGNORE_CHANGE_WINDOW); 52 | 53 | debug(`change detected "${getProjectPath(filePath)}"`); 54 | fn(path.resolve(filePath)); 55 | } 56 | 57 | changePending = false; 58 | }, CHANGE_DELAY); 59 | } 60 | }); 61 | 62 | return { 63 | has(filePath) { 64 | return files.has(resolveFilePath(filePath)); 65 | }, 66 | add(filePath) { 67 | if (filePath instanceof Set || Array.isArray(filePath)) { 68 | for (const file of filePath) { 69 | this.add(file); 70 | } 71 | return; 72 | } 73 | 74 | filePath = resolveFilePath(filePath); 75 | 76 | if ( 77 | !banned.has(filePath) && 78 | !files.has(filePath) && 79 | !filePath.startsWith(tmpdir) && 80 | !filePath.startsWith(config.dvlpDirPath) && 81 | !path.basename(filePath).startsWith('.') && 82 | !isNodeModuleFilePath(filePath) 83 | ) { 84 | debug(`watching file "${getProjectPath(filePath)}"`); 85 | files.add(filePath); 86 | watcher.add(filePath); 87 | } 88 | }, 89 | remove(filePath, permanent = false) { 90 | debug(`unwatching file "${getProjectPath(filePath)}"`); 91 | filePath = resolveFilePath(filePath); 92 | files.delete(filePath); 93 | watcher.unwatch(filePath); 94 | if (permanent) { 95 | banned.add(filePath); 96 | } 97 | }, 98 | close() { 99 | banned.clear(); 100 | files.clear(); 101 | watcher.close(); 102 | }, 103 | }; 104 | } 105 | 106 | /** 107 | * @param { string } filePath 108 | */ 109 | function resolveFilePath(filePath) { 110 | return path.resolve( 111 | filePath.startsWith('file://') ? fileURLToPath(filePath) : filePath, 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /test/browser/fixtures/mock/es.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "http://someapi.com/feed" 5 | }, 6 | "events": [ 7 | { 8 | "name": "open", 9 | "message": { 10 | "title": "open" 11 | }, 12 | "options": { 13 | "event": "foo" 14 | } 15 | }, 16 | { 17 | "name": "foo event", 18 | "message": { 19 | "title": "foo" 20 | }, 21 | "options": { 22 | "event": "foo" 23 | } 24 | }, 25 | { 26 | "name": "bar events", 27 | "sequence": [ 28 | { 29 | "message": "bar1", 30 | "options": { 31 | "delay": 500, 32 | "event": "bar" 33 | } 34 | }, 35 | { 36 | "message": "bar2", 37 | "options": { 38 | "delay": 1000, 39 | "event": "bar" 40 | } 41 | }, 42 | { 43 | "message": "bar3", 44 | "options": { 45 | "event": "bar" 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /test/browser/fixtures/mock/rest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://www.google.com/foo" 5 | }, 6 | "response": { 7 | "body": { 8 | "name": "foo" 9 | } 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /test/browser/fixtures/mock/ws.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "ws://someapi.com/socket" 5 | }, 6 | "events": [ 7 | { 8 | "name": "foo event", 9 | "message": { 10 | "title": "foo" 11 | } 12 | }, 13 | { 14 | "name": "bar events", 15 | "sequence": [ 16 | { 17 | "message": "bar1", 18 | "options": { 19 | "delay": 500 20 | } 21 | }, 22 | { 23 | "message": "bar2", 24 | "options": { 25 | "delay": 1000 26 | } 27 | }, 28 | { 29 | "message": "bar3" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DVLP: test-browser 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/css/adopted-doc-styles.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: aqua; 3 | } 4 | -------------------------------------------------------------------------------- /test/css/adopted-el-styles.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-block; 3 | } 4 | 5 | div { 6 | color: violet; 7 | } 8 | -------------------------------------------------------------------------------- /test/css/global-styles.css: -------------------------------------------------------------------------------- 1 | @import './imported-styles.css'; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | width: 100%; 7 | margin: 0; 8 | } 9 | 10 | html { 11 | color: chartreuse; 12 | } 13 | -------------------------------------------------------------------------------- /test/css/imported-level2-styles.css: -------------------------------------------------------------------------------- 1 | span { 2 | color: blueviolet; 3 | } 4 | -------------------------------------------------------------------------------- /test/css/imported-styles.css: -------------------------------------------------------------------------------- 1 | @import 'imported-level2-styles.css'; 2 | 3 | html { 4 | background-color: blanchedalmond; 5 | } 6 | -------------------------------------------------------------------------------- /test/css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test css injection 7 | 8 | 9 | 10 | 11 | 12 |
Some doc text
13 | 14 | 15 | -------------------------------------------------------------------------------- /test/css/index.js: -------------------------------------------------------------------------------- 1 | import './my-el.js'; 2 | import styles from './adopted-doc-styles.css' assert { type: 'css' }; 3 | 4 | document.adoptedStyleSheets = [styles]; 5 | -------------------------------------------------------------------------------- /test/css/my-el.js: -------------------------------------------------------------------------------- 1 | import styles from './adopted-el-styles.css' assert { type: 'css' }; 2 | 3 | const html = String.raw; 4 | 5 | class MyEl extends HTMLElement { 6 | constructor() { 7 | super(); 8 | const shadow = this.attachShadow({ mode: 'open' }); 9 | shadow.adoptedStyleSheets = [styles]; 10 | } 11 | 12 | connectedCallback() { 13 | this.shadowRoot.innerHTML = html`

Some Title

Some Text
`; 14 | } 15 | } 16 | 17 | customElements.define('my-el', MyEl); 18 | -------------------------------------------------------------------------------- /test/electron/entry-load-file-with-fetch-mock.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | await app.whenReady(); 4 | 5 | new BrowserWindow({ width: 800, height: 600 }).loadFile('renderer.html'); 6 | 7 | const res = await fetch('https://www.someapi.com/v1/9012'); 8 | const json = await res.json(); 9 | 10 | console.log(json); 11 | -------------------------------------------------------------------------------- /test/electron/entry-load-file.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | await app.whenReady(); 4 | 5 | new BrowserWindow({ width: 800, height: 600 }).loadFile('renderer.html'); 6 | -------------------------------------------------------------------------------- /test/electron/entry-server-worker.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import { dirname, join } from 'node:path'; 3 | import { MessageChannel, Worker } from 'node:worker_threads'; 4 | import { fileURLToPath } from 'node:url'; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | await app.whenReady(); 9 | 10 | const { port1, port2 } = new MessageChannel(); 11 | 12 | port1.unref(); 13 | 14 | await new Promise((resolve, reject) => { 15 | new Worker(join(__dirname, 'worker.js'), { 16 | transferList: [port2], 17 | workerData: { 18 | messagePort: port2, 19 | }, 20 | }); 21 | 22 | port1.on('message', (msg) => { 23 | if (msg === 'listening') { 24 | resolve(); 25 | } 26 | }); 27 | }); 28 | 29 | new BrowserWindow({ width: 800, height: 600 }).loadURL('http://localhost:8100'); 30 | -------------------------------------------------------------------------------- /test/electron/entry-server.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import fastify from 'fastify'; 3 | import template from './template.js'; 4 | 5 | await app.whenReady(); 6 | 7 | const server = fastify(); 8 | 9 | server.get('/', async (req, reply) => { 10 | reply.type('text/html').send(template); 11 | }); 12 | 13 | await server.listen({ port: 8100 }); 14 | 15 | new BrowserWindow({ width: 800, height: 600 }).loadURL('http://localhost:8100'); 16 | -------------------------------------------------------------------------------- /test/electron/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "https://www.someapi.com/v1/9012" 4 | }, 5 | "response": { 6 | "body": { 7 | "user": { 8 | "name": "Bob", 9 | "id": 9012 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/electron/renderer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Electron 8 | 9 | 10 | 11 |

Hi electron!

12 | 13 | 14 | -------------------------------------------------------------------------------- /test/electron/renderer.js: -------------------------------------------------------------------------------- 1 | import { html, render } from 'lit-html'; 2 | 3 | render(html`

yay!

`, document.body); 4 | -------------------------------------------------------------------------------- /test/electron/template.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | 5 | 6 | 7 | Electron 8 | 9 | 10 | 11 |

Hi electron!

12 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /test/electron/worker.js: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify'; 2 | import template from './template.js'; 3 | import { workerData } from 'worker_threads'; 4 | 5 | const server = fastify(); 6 | 7 | server.get('/', async (req, reply) => { 8 | reply.type('text/html').send(template); 9 | }); 10 | 11 | await server.listen({ port: 8100 }); 12 | 13 | workerData.messagePort.postMessage('listening'); 14 | -------------------------------------------------------------------------------- /test/integration/fixtures/app.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | createServer((req, res) => { 4 | res.writeHead(200); 5 | res.end('hi'); 6 | }).listen(process.env.PORT || 8080); 7 | -------------------------------------------------------------------------------- /test/integration/fixtures/assets/a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/integration/fixtures/assets/a.js -------------------------------------------------------------------------------- /test/integration/fixtures/www/a.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/integration/fixtures/www/a.css -------------------------------------------------------------------------------- /test/integration/server-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { fork } from 'node:child_process'; 3 | 4 | /** @type { import('child_process').ChildProcess */ 5 | let childProcess; 6 | 7 | describe('server', () => { 8 | afterEach(() => { 9 | childProcess && childProcess.kill(); 10 | }); 11 | 12 | describe('static', () => { 13 | it('should serve static files from single directory', async () => { 14 | childProcess = await child('bin/dvlp.js', [ 15 | 'test/integration/fixtures/assets', 16 | ]); 17 | const res = await fetch('http://localhost:8080/a.js'); 18 | expect(res.status).to.eql(200); 19 | expect(res.headers.get('Content-type')).to.include( 20 | 'application/javascript', 21 | ); 22 | }); 23 | it('should serve static files from multiple directories', async () => { 24 | childProcess = await child('bin/dvlp.js', [ 25 | 'test/integration/fixtures/assets', 26 | 'test/integration/fixtures/www', 27 | ]); 28 | let res = await fetch('http://localhost:8080/a.js'); 29 | expect(res.status).to.eql(200); 30 | expect(res.headers.get('Content-type')).to.include( 31 | 'application/javascript', 32 | ); 33 | res = await fetch('http://localhost:8080/a.css'); 34 | expect(res.status).to.eql(200); 35 | expect(res.headers.get('Content-type')).to.include('text/css'); 36 | }); 37 | }); 38 | 39 | describe('application', () => { 40 | it('should start app server', async () => { 41 | childProcess = await child('bin/dvlp.js', [ 42 | 'test/integration/fixtures/app.mjs', 43 | ]); 44 | const res = await fetch('http://localhost:8080/', { 45 | headers: { accept: 'text/html' }, 46 | }); 47 | expect(res.status).to.eql(200); 48 | expect(await res.text()).to.contain('hi'); 49 | }); 50 | }); 51 | }); 52 | 53 | function child(...args) { 54 | return new Promise((resolve, reject) => { 55 | const childProcess = fork(...args); 56 | setTimeout(() => { 57 | resolve(childProcess); 58 | }, 2000); 59 | childProcess.on('error', reject); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /test/integration/test-server-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { testServer } from 'dvlp/test'; 3 | 4 | let server; 5 | 6 | describe('testServer', () => { 7 | before(() => { 8 | testServer.disableNetwork(); 9 | }); 10 | afterEach(async () => { 11 | server && (await server.destroy()); 12 | }); 13 | after(() => { 14 | testServer.enableNetwork(); 15 | }); 16 | 17 | it('should respond to requests for fake resources', async () => { 18 | server = await testServer({ autorespond: true, port: 8888 }); 19 | const res = await fetch('http://localhost:8888/foo.js'); 20 | expect(res).to.exist; 21 | expect(await res.text()).to.contain('hello'); 22 | }); 23 | it('should respond with 404 when "?missing"', async () => { 24 | server = await testServer({ port: 8888 }); 25 | const res = await fetch('http://localhost:8888/foo.js?missing'); 26 | expect(res).to.exist; 27 | expect(res.status).to.equal(404); 28 | }); 29 | it('should throw when making an external request and network disabled', async () => { 30 | try { 31 | const res = await fetch('http://www.google.com'); 32 | expect(res).to.not.exist; 33 | } catch (err) { 34 | expect(err).to.exist; 35 | expect(err.message).to.equal( 36 | 'network connections disabled. Unable to request http://www.google.com/', 37 | ); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/unit/electron-test.js: -------------------------------------------------------------------------------- 1 | import { cleanBundledFiles } from '../../src/utils/bundling.js'; 2 | import config from '../../src/config.js'; 3 | import { server as serverFactory } from '../../src/dvlp.js'; 4 | 5 | let server; 6 | 7 | // TODO: Missing X server or $DISPLAY 8 | if (!process.env.CI) { 9 | describe('electron', () => { 10 | beforeEach(() => { 11 | cleanBundledFiles(); 12 | }); 13 | afterEach(async () => { 14 | config.directories = [process.cwd()]; 15 | cleanBundledFiles(); 16 | server && (await server.destroy()); 17 | }); 18 | 19 | it('should start an electron app with loadFile()', (done) => { 20 | serverFactory('test/unit/fixtures/electron-file.mjs', { 21 | electron: true, 22 | port: 8100, 23 | reload: false, 24 | }).then((srvr) => { 25 | server = srvr; 26 | srvr.electronProcess.activeProcess.on('message', (msg) => { 27 | if (msg === 'test:done') { 28 | done(); 29 | } 30 | }); 31 | }); 32 | }); 33 | it('should start an electron app with internal server and loadURL()', (done) => { 34 | serverFactory('test/unit/fixtures/electron-create-server.mjs', { 35 | electron: true, 36 | port: 8100, 37 | reload: false, 38 | }).then((srvr) => { 39 | server = srvr; 40 | srvr.electronProcess.activeProcess.on('message', (msg) => { 41 | if (msg === 'test:done') { 42 | done(); 43 | } 44 | }); 45 | }); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-api.mjs: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify'; 2 | 3 | const server = fastify(); 4 | 5 | server.get('/', async (req, reply) => { 6 | const res = await fetch('https://www.someapi.com/v1/9012'); 7 | const { user } = await res.json(); 8 | 9 | reply.type('text/html').send(` 10 | 11 | 12 | 13 | 14 | 15 | 16 | ${user.name} 17 | 18 | `); 19 | }); 20 | 21 | server.listen({ port: 8100 }, (err, address) => { 22 | err && console.error(err); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-create-server.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | createServer((req, res) => { 4 | res.writeHead(200); 5 | res.end('ok'); 6 | }).listen('localhost:8100'); 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-error.mjs: -------------------------------------------------------------------------------- 1 | import body from './body.mjs'; 2 | import fastify from 'fastify'; 3 | 4 | const server = fastify(); 5 | 6 | oops; 7 | 8 | server.get('/', async (req, reply) => { 9 | reply.type('text/html').send(` 10 | 11 | 12 | 13 | 14 | 15 | 16 | ${body} 17 | 18 | `); 19 | }); 20 | 21 | server.listen({ port: 8100 }, (err, address) => { 22 | err && console.error(err); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-https.mjs: -------------------------------------------------------------------------------- 1 | import body from './body.mjs'; 2 | import fastify from 'fastify'; 3 | import { fileURLToPath } from 'node:url'; 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | const server = fastify({ 9 | http2: true, 10 | https: { 11 | allowHTTP1: true, 12 | key: fs.readFileSync( 13 | path.join(__dirname, 'certificates/dvlp.key'), 14 | 'utf-8', 15 | ), 16 | cert: fs.readFileSync( 17 | path.join(__dirname, 'certificates/dvlp.crt'), 18 | 'utf-8', 19 | ), 20 | }, 21 | }); 22 | 23 | server.get('/', async (req, reply) => { 24 | reply.type('text/html').send(` 25 | 26 | 27 | 28 | 29 | 30 | 31 | ${body} 32 | 33 | `); 34 | }); 35 | 36 | server.listen({ port: 3333 }, (err, address) => { 37 | err && console.error(err); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-listener.mjs: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | const server = createServer(); 4 | 5 | server.on('request', (req, res) => { 6 | res.writeHead(200); 7 | res.end('ok'); 8 | }); 9 | 10 | server.listen(8100); 11 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-multi.mjs: -------------------------------------------------------------------------------- 1 | import body from './body.mjs'; 2 | import fastify from 'fastify'; 3 | 4 | const server1 = fastify(); 5 | const server2 = fastify(); 6 | 7 | server1.get('*', async (req, reply) => { 8 | reply.callNotFound(); 9 | }); 10 | 11 | server1.listen({ port: 8101 }); 12 | 13 | server2.get('/', async (req, reply) => { 14 | reply.type('text/html').send(` 15 | 16 | 17 | 18 | 19 | 20 | 21 | ${body} 22 | 23 | `); 24 | }); 25 | 26 | server2.listen({ port: 8100 }); 27 | -------------------------------------------------------------------------------- /test/unit/fixtures/app-request-error.mjs: -------------------------------------------------------------------------------- 1 | import { fastify } from 'fastify'; 2 | 3 | const server = fastify(); 4 | 5 | server.get('/', async (req, reply) => { 6 | reply.type('text/html').send(` 7 | 8 | 9 | 10 | 11 | 12 | 13 | ${errrrrrrrrrrrr} 14 | 15 | `); 16 | }); 17 | 18 | server.listen({ port: 8100 }, (err, address) => { 19 | err && console.error(err); 20 | }); 21 | -------------------------------------------------------------------------------- /test/unit/fixtures/app.mjs: -------------------------------------------------------------------------------- 1 | import body from './body.mjs'; 2 | import fastify from 'fastify'; 3 | 4 | const server = fastify(); 5 | 6 | server.get('/', async (req, reply) => { 7 | reply.type('text/html').send(` 8 | 9 | 10 | 11 | 12 | 13 | 14 | ${body} 15 | 16 | `); 17 | }); 18 | 19 | server.listen({ port: 8100 }, (err, address) => { 20 | err && console.error(err); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/fixtures/app.ts: -------------------------------------------------------------------------------- 1 | import body from './body'; 2 | import fastify from 'fastify'; 3 | 4 | const server = fastify(); 5 | 6 | server.get('/', async (req, reply) => { 7 | reply.type('text/html').send(` 8 | 9 | 10 | 11 | 12 | 13 | 14 | ${body} 15 | 16 | `); 17 | }); 18 | 19 | server.listen({ port: 8100 }, (err, address) => { 20 | err && console.error(err); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/fixtures/assets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | } -------------------------------------------------------------------------------- /test/unit/fixtures/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/body.mjs: -------------------------------------------------------------------------------- 1 | export default 'hi'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/body.ts: -------------------------------------------------------------------------------- 1 | export default 'hi'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/certificates/dvlp.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICtDCCAZygAwIBAgIJAOD+hH5pcM2GMA0GCSqGSIb3DQEBBQUAMA8xDTALBgNV 3 | BAMTBGR2bHAwHhcNMjAwOTE2MTUwMjQzWhcNMzAwOTE0MTUwMjQzWjAPMQ0wCwYD 4 | VQQDEwRkdmxwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIYCODPz 5 | /TvNWmHxLBF+2w1z1hZN+Du594borDePZTVxPFxgE9/1vUhL/bRBNt7YWl36ANf6 6 | 4ECqVVTSztp2RmnRvng4erePsZhXybj6zP/qgdfKFhFu9PpG1UejHNpmdL0xKums 7 | 1I6gg6R9GTawW3FMvJL+DKi4jc629lhsMQWBAs42UdTTO36yOPJNx1PfUoFCh/Gf 8 | qg46wuKgtgNnEN5MmYlVa7taPfvCdbDqsWAa+L4M3LtD2eXOYqtKzwKFAaNOIezs 9 | e5e36g2RJ1qmq2FQ1LnqMpuKJjwsrumb9mcEF+4f+txYwfiV4Hy2cP14eLHmrVSk 10 | FgHBiJ+JKUwELwIDAQABoxMwETAPBgNVHREECDAGggRkdmxwMA0GCSqGSIb3DQEB 11 | BQUAA4IBAQBxkSDmei1Q80u1BpD/bsvEnU1ttTMRdYBlpFnY3plIS+n/IH7r4Xow 12 | dj7x9mC3gZx8PabeRGFhVrh6TlyRDPGb0OUNzTMoxiA9ak1943yfnXsXomFXSq1c 13 | F0RGbKXzqtX1dMX3hGTfGz/Prbngx8yKXC4mX0X5wwIGRGd4xdxIA43zVjvGOODK 14 | snahC5zeT8sDFFfADcY49EsFtv0oMKsXf3DgvEKX2t7ZLhION1+ZKv4Sr+Cfoz6e 15 | CLhrJENSMTuX6BOUgiVLlWh7GzXdewwZJqLo2T/XL//xoWVDEYb7dQhW0qYq6Gqp 16 | v2g06lIBAvbeQ9gEvVXVguddl7JcQfIU 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /test/unit/fixtures/certificates/dvlp.issuer.crt: -------------------------------------------------------------------------------- 1 | 2 | -----BEGIN CERTIFICATE----- 3 | MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ 4 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT 5 | DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow 6 | SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT 7 | GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC 8 | AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF 9 | q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 10 | SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 11 | Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA 12 | a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj 13 | /PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T 14 | AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG 15 | CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv 16 | bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k 17 | c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw 18 | VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC 19 | ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz 20 | MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu 21 | Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF 22 | AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo 23 | uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ 24 | wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu 25 | X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG 26 | PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 27 | KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/unit/fixtures/certificates/dvlp.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAnIYCODPz/TvNWmHxLBF+2w1z1hZN+Du594borDePZTVxPFxg 3 | E9/1vUhL/bRBNt7YWl36ANf64ECqVVTSztp2RmnRvng4erePsZhXybj6zP/qgdfK 4 | FhFu9PpG1UejHNpmdL0xKums1I6gg6R9GTawW3FMvJL+DKi4jc629lhsMQWBAs42 5 | UdTTO36yOPJNx1PfUoFCh/Gfqg46wuKgtgNnEN5MmYlVa7taPfvCdbDqsWAa+L4M 6 | 3LtD2eXOYqtKzwKFAaNOIezse5e36g2RJ1qmq2FQ1LnqMpuKJjwsrumb9mcEF+4f 7 | +txYwfiV4Hy2cP14eLHmrVSkFgHBiJ+JKUwELwIDAQABAoIBAFA4DiMn3UBHyfMs 8 | hyICxXUW6+so+0Tht2m45r58qy0/uo0o+sS034jm6KtaGqI4i8GksGCGULll6uZY 9 | 1sHVDREBYtGvY6LhOO/YGAX2m/M8pb9uDNBKlwdOzca5NEuxUxk5bV2E7WLyxikx 10 | wDuP38q9wopS+4kZX5yt+O9AWhK05jYHDxVrj5E6zwy3EfnJcTEgNUdwy+Htb9oi 11 | rsorzDTHwwzh/HuoMMqJvCtFEoO/uh90tuipRd4mocMUloiEO3se+Ia0uqK/kFrq 12 | sNg5vSg5j+dbeAvBzwG72FMgjx1aS6SW7FqJPajPVX6dAz1n0YRcDY3etSqoi2eV 13 | +kZEW8ECgYEAy9ItxTzQOeqkwvkxmJb9Hf3TUmvJ+DFFvEhWmpSgVr2hHMefD5u2 14 | AVdLv8dMDBFChoEtLGVrNhvWetU4NHpl7xZx9J6cBO4D/tpFNO4qlUG4qFo3SCZj 15 | vO+u5wL+Kc/b0eUhMnMJ0tqvOO9mknhVx9gLIlYke22nof+3e9fAyPcCgYEAxJgX 16 | FsaVxVVIyJCHAXYBnNFzz6yl2GnN0NPa03RSlScnqgmrCDdWInX88HE6PC8dBFST 17 | W9Y3tKnfpS8S+srRyRyIXMrze1u+kGdJsDFsoqNhcHd/MUgsAzWN9Sm7r6DgrJzQ 18 | o9zWssPKw10Nd9KlE4a5tyABJ7O9vT5TE/Z9SIkCgYEAjjL94dzSvYV6G9k1g+rb 19 | f0AmXht7ll/x8jFZ0pEc6Ed2jxiqXX5aVccsvwjxgn3MNwEKni3HxcFYOuxlQR+f 20 | 3FWBfZPm7/2K5hQsMohzRxzKExKV/Q1jil6CXQOWhV9SUrcUGRlvYh9WHlfP7SJt 21 | XnbZFcSZwU84o+o/ffSBuPMCgYEAsjlDLUWov3W6fwDvM3bcrWMAv7O/wfrhOEDn 22 | b61TtJ4DilYrdE5eSu11+jBb3/XCM4vM74O7ipA6DNEpPq0iFFVGlgDzTND2aIkK 23 | t62G08aT7laWu4G3TM1/PVOxL94D8NhVGUh6ZyOyrPut2wPe3V3U/VwJUAnVqDtZ 24 | K47ZMykCgYEApPlqigagKv4Iy1qgM6wB4GbBpSqKiuRsYmSDIVc1lvA4vGpAVBC1 25 | RZ/pingHHz+IxIKnIRKHCAu1yskyiEXUvgKRY1Hdx2lf2Vru31OsfgUihAThtOUf 26 | hH5MlYQeM4wZQEIEPSbK6LsiDEC8uG0rRKD6ikcpbm1D5yvnv+QNV2I= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/unit/fixtures/component.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars:0 */ 2 | import React from 'react'; 3 | 4 | module.exports =
; 5 | -------------------------------------------------------------------------------- /test/unit/fixtures/electron-create-server.mjs: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron'; 2 | import { createServer } from 'http'; 3 | import fs from 'node:fs'; 4 | import path from 'node:path'; 5 | 6 | await app.whenReady(); 7 | 8 | ipcMain.on('done', () => { 9 | setTimeout(() => { 10 | process.send('test:done'); 11 | }, 100); 12 | }); 13 | 14 | createServer((req, res) => { 15 | res.writeHead(200); 16 | res.end( 17 | fs.readFileSync( 18 | path.resolve('./test/unit/fixtures/electron/index.html'), 19 | 'utf8', 20 | ), 21 | ); 22 | }).listen('localhost:8100'); 23 | 24 | const window = new BrowserWindow({ 25 | show: false, 26 | webPreferences: { 27 | contextIsolation: false, 28 | sandbox: false, 29 | preload: path.resolve('./test/unit/fixtures/electron/preload.cjs'), 30 | }, 31 | }); 32 | 33 | await window.loadURL('http://localhost:8100'); 34 | -------------------------------------------------------------------------------- /test/unit/fixtures/electron-file.mjs: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron'; 2 | import path from 'node:path'; 3 | 4 | await app.whenReady(); 5 | 6 | ipcMain.on('done', () => { 7 | setTimeout(() => { 8 | process.send('test:done'); 9 | }, 100); 10 | }); 11 | 12 | const window = new BrowserWindow({ 13 | show: false, 14 | webPreferences: { 15 | contextIsolation: false, 16 | sandbox: false, 17 | preload: path.resolve('./test/unit/fixtures/electron/preload.cjs'), 18 | }, 19 | }); 20 | 21 | await window.loadFile('./test/unit/fixtures/electron/index.html'); 22 | -------------------------------------------------------------------------------- /test/unit/fixtures/electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/unit/fixtures/electron/preload.cjs: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | 3 | window.done = () => { 4 | ipcRenderer.send('done'); 5 | }; 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/file.esm.js: -------------------------------------------------------------------------------- 1 | import dep from './www/dep-esm.js'; 2 | 3 | export default { 4 | dep, 5 | }; 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/file.js: -------------------------------------------------------------------------------- 1 | const dep = require('./www/dep-cjs.js'); 2 | 3 | module.exports = { 4 | dep, 5 | }; 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-bundle.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | onDependencyBundle(id, filePath) { 5 | return `this is bundled content for: ${path.basename(filePath)}`; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-error.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | onTransform(filePath, code) { 5 | throw Error(`transform error ${path.basename(filePath)}`); 6 | }, 7 | onSend(filePath, code) {}, 8 | onServerTransform(filePath, code) {}, 9 | }; 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-request.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | onRequest(req, res) { 3 | if (req.url === '/api') { 4 | res.writeHead(200); 5 | res.end('handled'); 6 | return true; 7 | } 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-send.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | onSend(filePath, code) { 5 | return `this is sent content for: ${path.basename(filePath)}`; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-transform-bundle.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | async onTransform(filePath, code, context) { 3 | return ( 4 | await context.esbuild.build({ 5 | bundle: true, 6 | format: 'esm', 7 | entryPoints: [filePath], 8 | write: false, 9 | }) 10 | ).outputFiles[0].text; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-transform-server.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | onServerTransform(url, context, defaultLoad) { 3 | if (url.endsWith('body.ts')) { 4 | return { format: 'module', source: 'export default "hi from body hook";' }; 5 | } 6 | return defaultLoad(url, context); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/hooks-transform.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | async onTransform(filePath, code, context) { 5 | await sleep(200); 6 | return `this is transformed content for: ${path.basename(filePath)} on ${context.client.name}:${ 7 | context.client.version 8 | }`; 9 | }, 10 | }; 11 | 12 | function sleep(duration) { 13 | return new Promise((resolve) => { 14 | setTimeout(resolve, duration); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock-push-connect/event-source.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "https://localhost:8111/feed" 5 | }, 6 | "events": [ 7 | { 8 | "name": "foo event", 9 | "message": { 10 | "title": "foo" 11 | }, 12 | "options": { 13 | "connect": true, 14 | "event": "foo" 15 | } 16 | }, 17 | { 18 | "name": "bar event", 19 | "message": { 20 | "title": "bar" 21 | }, 22 | "options": { 23 | "connect": true, 24 | "event": "bar" 25 | } 26 | } 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock-push-connect/web-socket.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "ws://localhost:8111/socket" 5 | }, 6 | "events": [ 7 | { 8 | "name": "foo event", 9 | "message": "foo", 10 | "options": { 11 | "connect": true 12 | } 13 | }, 14 | { 15 | "name": "bar event", 16 | "message": "bar", 17 | "options": { 18 | "connect": true 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock-push/event-source.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "https://localhost:8111/feed" 5 | }, 6 | "events": [ 7 | { 8 | "name": "open", 9 | "message": { 10 | "title": "open" 11 | }, 12 | "options": { 13 | "event": "foo" 14 | } 15 | }, 16 | { 17 | "name": "foo event", 18 | "message": { 19 | "title": "foo" 20 | }, 21 | "options": { 22 | "event": "foo" 23 | } 24 | }, 25 | { 26 | "name": "bar events", 27 | "sequence": [ 28 | { 29 | "message": "bar1", 30 | "options": { 31 | "delay": 500, 32 | "event": "bar" 33 | } 34 | }, 35 | { 36 | "message": "bar2", 37 | "options": { 38 | "delay": 1000, 39 | "event": "bar" 40 | } 41 | }, 42 | { 43 | "message": "bar3", 44 | "options": { 45 | "event": "bar" 46 | } 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock-push/web-socket.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "stream": { 4 | "url": "ws://localhost:8111/socket" 5 | }, 6 | "events": [ 7 | { 8 | "name": "foo event", 9 | "message": { 10 | "title": "foo" 11 | } 12 | }, 13 | { 14 | "name": "bar events", 15 | "sequence": [ 16 | { 17 | "message": "bar1", 18 | "options": { 19 | "delay": 500 20 | } 21 | }, 22 | { 23 | "message": "bar2", 24 | "options": { 25 | "delay": 1000 26 | } 27 | }, 28 | { 29 | "message": "bar3" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/1234.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/mock/1234.jpg -------------------------------------------------------------------------------- /test/unit/fixtures/mock/1234.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/1234.jpg" 4 | }, 5 | "response": { 6 | "body": "1234.jpg", 7 | "headers": { "x-foo": "foo" } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/4567.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | request: { 3 | url: 'https://www.someapi.com/v1/4567', 4 | }, 5 | response: (req, res) => { 6 | const content = JSON.stringify({ user: { name: 'Gus', id: 4567 } }); 7 | res.writeHead(200, { 8 | 'Content-Type': 'application/json', 9 | Date: new Date().toUTCString(), 10 | 'Content-Length': Buffer.byteLength(content), 11 | 'Access-Control-Allow-Origin': '*', 12 | }); 13 | res.end(content); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/5678.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "http://www.someapi.com/v1/5678", 4 | "ignoreSearch": true 5 | }, 6 | "response": { 7 | "headers": { 8 | "x-custom": "custom header", 9 | "date": "Fri, 13 Oct 2020 23:59:59 GMT" 10 | }, 11 | "body": { 12 | "user": { 13 | "name": "Nancy", 14 | "id": 5678 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/9012.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "https://www.someapi.com/v1/9012" 4 | }, 5 | "response": { 6 | "body": { 7 | "user": { 8 | "name": "Bob", 9 | "id": 9012 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/json.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "/json" 4 | }, 5 | "response": { 6 | "body": "./test.json" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/more/3456.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "https://www.someapi.com/v1/3456" 4 | }, 5 | "response": { 6 | "body": { 7 | "user": { 8 | "name": "Harvey", 9 | "id": 3456 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/multi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request": { 4 | "url": "http://www.someapi.com/v2/5678", 5 | "ignoreSearch": true 6 | }, 7 | "response": { 8 | "headers": { 9 | "x-custom": "custom header" 10 | }, 11 | "body": { 12 | "user": { 13 | "name": "Nancy", 14 | "id": 5678 15 | } 16 | } 17 | } 18 | }, 19 | { 20 | "request": { 21 | "url": "https://www.someapi.com/v2/9012" 22 | }, 23 | "response": { 24 | "body": { 25 | "user": { 26 | "name": "Bob", 27 | "id": 9012 28 | } 29 | } 30 | } 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/params.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "http://www.someapi.com/v3/params/:param1/:param2\\?foo=foo&bar=bar" 4 | }, 5 | "response": { 6 | "body": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "url": "http://www.someapi.com/v1/search?foo=foo&bar=bar" 4 | }, 5 | "response": { 6 | "body": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/mock/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "test" 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/.modules.yaml: -------------------------------------------------------------------------------- 1 | hoistPattern: 2 | - '*' 3 | hoistedDependencies: {} 4 | included: 5 | dependencies: true 6 | devDependencies: true 7 | optionalDependencies: true 8 | layoutVersion: 5 9 | packageManager: pnpm@7.25.1 10 | pendingBuilds: [] 11 | prunedAt: Sun, 22 Jan 2023 11:44:32 GMT 12 | publicHoistPattern: 13 | - '*eslint*' 14 | - '*prettier*' 15 | registries: 16 | default: https://registry.npmjs.org/ 17 | skipped: [] 18 | storeDir: /Users/alex/Library/pnpm/store/v3 19 | virtualStoreDir: .pnpm 20 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bar/browser.js: -------------------------------------------------------------------------------- 1 | export const bar = 'hello from browser'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bar/index.js: -------------------------------------------------------------------------------- 1 | export const bar = 'hello from index'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bar", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "browser": "browser.js" 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bat/boo/index.esm.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/node_modules/bat/boo/index.esm.js -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bat/boo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "module": "index.esm.js" 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bat/browser.js: -------------------------------------------------------------------------------- 1 | export const bat = 'hello from browser'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bat/index.js: -------------------------------------------------------------------------------- 1 | export const bat = 'hello from index'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/bat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bat", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "browser": { 6 | "./index.js": "./browser.js" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css", 3 | "version": "1.0.0", 4 | "main": "styles.css" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/css/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/node_modules/css/styles.css -------------------------------------------------------------------------------- /test/unit/fixtures/node_modules/foo/foo.js: -------------------------------------------------------------------------------- 1 | console.log('this is foo'); 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-fixtures", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/baz.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/baz.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/dir/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/dir/index.ts -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/foo.bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/foo.bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/linked/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/linked/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/linked/node_modules/.modules.yaml: -------------------------------------------------------------------------------- 1 | hoistPattern: 2 | - '*' 3 | hoistedDependencies: {} 4 | included: 5 | dependencies: true 6 | devDependencies: true 7 | optionalDependencies: true 8 | layoutVersion: 5 9 | packageManager: pnpm@7.25.1 10 | pendingBuilds: [] 11 | prunedAt: Sun, 22 Jan 2023 11:44:32 GMT 12 | publicHoistPattern: 13 | - '*eslint*' 14 | - '*prettier*' 15 | registries: 16 | default: https://registry.npmjs.org/ 17 | skipped: [] 18 | storeDir: /Users/alex/Library/pnpm/store/v3 19 | virtualStoreDir: .pnpm 20 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/linked/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linked", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/nested/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/nested/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/nested/nested/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/nested/nested/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/nested/node_modules/.modules.yaml: -------------------------------------------------------------------------------- 1 | hoistPattern: 2 | - '*' 3 | hoistedDependencies: {} 4 | included: 5 | dependencies: true 6 | devDependencies: true 7 | optionalDependencies: true 8 | layoutVersion: 5 9 | packageManager: pnpm@7.25.1 10 | pendingBuilds: [] 11 | prunedAt: Sun, 22 Jan 2023 11:44:32 GMT 12 | publicHoistPattern: 13 | - '*eslint*' 14 | - '*prettier*' 15 | registries: 16 | default: https://registry.npmjs.org/ 17 | skipped: [] 18 | storeDir: /Users/alex/Library/pnpm/store/v3 19 | virtualStoreDir: .pnpm 20 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/nested/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nested", 3 | "version": "1.0.0", 4 | "main": "foo.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/.modules.yaml: -------------------------------------------------------------------------------- 1 | hoistPattern: 2 | - '*' 3 | hoistedDependencies: {} 4 | included: 5 | dependencies: true 6 | devDependencies: true 7 | optionalDependencies: true 8 | layoutVersion: 5 9 | packageManager: pnpm@7.25.1 10 | pendingBuilds: [] 11 | prunedAt: Sun, 22 Jan 2023 11:44:32 GMT 12 | publicHoistPattern: 13 | - '*eslint*' 14 | - '*prettier*' 15 | registries: 16 | default: https://registry.npmjs.org/ 17 | skipped: [] 18 | storeDir: /Users/alex/Library/pnpm/store/v3 19 | virtualStoreDir: .pnpm 20 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/.pnpm/a@1.0.0/node_modules/a/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/.pnpm/a@1.0.0/node_modules/a/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/.pnpm/a@1.0.0/node_modules/a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/lib/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/lib/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/foo/lib/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/foo/lib/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/foo/lib/bat.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/foo/lib/bat.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "2.0.0", 4 | "main": "./lib/bat.js" 5 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/versioned/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/versioned/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/node_modules/versioned/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "versioned", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@popeindustries/test", 3 | "version": "1.0.0", 4 | "description": "test project", 5 | "main": "test.js" 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/test.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/test.css -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/@popeindustries/test/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/@popeindustries/test/test.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/a: -------------------------------------------------------------------------------- 1 | ./.pnpm/a@1.0.0/node_modules/a -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/alias/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/alias/index.ts -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/alias/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alias", 3 | "main": "index", 4 | "browser": { 5 | "./index.js": "./index.mjs" 6 | }, 7 | "version": "1.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bar/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/bar/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bar/node_modules/boo/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'bar'; -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bar/node_modules/boo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boo", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bat/boo/index.esm.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/bat/boo/index.esm.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bat/boo/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/bat/boo/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bat/boo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "module": "index.esm.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bat/index.js: -------------------------------------------------------------------------------- 1 | export const bat = 'hello from index'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/bat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bat", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/bing.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/bing.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/browser/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/browser/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bar/lib/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bar/lib/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bar/lib/bat.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bar/lib/bat.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bar", 3 | "version": "2.0.0", 4 | "main": "./lib/bat.js", 5 | "browser": "./lib/bar.js" 6 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bat/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bat/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bat", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bing/bing.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bing/bing.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bing/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bing/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/bing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bing", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/foo/lib/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/foo/lib/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/foo/lib/bat.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/foo/lib/bat.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "2.0.0", 4 | "main": "./lib/bat.js" 5 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-hash", 3 | "version": "1.0.0", 4 | "main": "./server/foo.js", 5 | "browser": { 6 | "./server/foo.js": "./browser/foo.js", 7 | "index": "./browser/foo.js", 8 | "bat": false, 9 | "foo": "bar", 10 | "bar": "./foo.js", 11 | "./bar.js": false, 12 | "./bing.js": "bing", 13 | "http": false, 14 | "net": "./foo.js", 15 | "./test.js": "@popeindustries/test" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/server/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/server/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser-hash/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser-hash/test.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser/browser/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser/browser/foo.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/browser/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser", 3 | "version": "1.0.0", 4 | "main": "./index.js", 5 | "browser": "./browser/foo.js" 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/browser.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/browser.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/main.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/nested/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/nested/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/nested/only-ts.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/nested/only-ts.ts -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exports", 3 | "main": "main.js", 4 | "exports": { 5 | ".": { 6 | "browser": "./browser.js", 7 | "import": "./main.js" 8 | }, 9 | "./sub": { 10 | "development": { 11 | "browser": "./sub/sub-browser-dev.js", 12 | "import": "./sub/sub-dev.js" 13 | }, 14 | "browser": "./sub/sub-browser.js", 15 | "import": "./sub/sub.js" 16 | }, 17 | "./nested/*": "./nested/*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/sub/sub-browser-dev.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/sub/sub-browser-dev.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/sub/sub-browser.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/sub/sub-browser.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/sub/sub-dev.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/sub/sub-dev.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/exports/sub/sub.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/exports/sub/sub.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo-dir/lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo-dir/lib/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo-dir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo-dir", 3 | "version": "1.0.0", 4 | "main": "lib" 5 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/lib/bar.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo/lib/bar.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/lib/bat.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo/lib/bat.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/lib/caseSensitive.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo/lib/caseSensitive.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/node_modules/bar/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 'bar'; -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/node_modules/bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bar", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/node_modules/versioned/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/foo/node_modules/versioned/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/node_modules/versioned/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "versioned", 3 | "version": "2.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "1.0.0", 4 | "main": "lib/bat.js" 5 | } -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/linked: -------------------------------------------------------------------------------- 1 | ../linked -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module-browser/browser.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/module-browser/browser.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module-browser/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/module-browser/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module-browser/index.mjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/module-browser/index.mjs -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module-browser", 3 | "module": "index.mjs", 4 | "main": "index.js", 5 | "browser": { 6 | "./index.js": "./browser.js", 7 | "./index.mjs": "./browser.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/module/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module/index.mjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/module/index.mjs -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "module", 3 | "module": "index.mjs", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/versioned/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/resolver/node_modules/versioned/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/node_modules/versioned/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "versioned", 3 | "version": "1.0.0", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/resolver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-project", 3 | "version": "1.0.0", 4 | "imports": { 5 | "#foo": "./foo.bar.js", 6 | "#dep": { 7 | "development": "bar" 8 | } 9 | }, 10 | "exports": { 11 | ".": "./baz.js", 12 | "./foo.js": "./foo.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/fixtures/route.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/route.ts -------------------------------------------------------------------------------- /test/unit/fixtures/script.js: -------------------------------------------------------------------------------- 1 | console.log('just a script file'); 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/$$.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/$$.js -------------------------------------------------------------------------------- /test/unit/fixtures/www/dep-cjs.js: -------------------------------------------------------------------------------- 1 | module.exports = 'HI!'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/dep-esm.js: -------------------------------------------------------------------------------- 1 | export default 'HI!'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/dep.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/dep.css -------------------------------------------------------------------------------- /test/unit/fixtures/www/dep.ts: -------------------------------------------------------------------------------- 1 | export default 'HI!'; 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/dep2.js: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | 3 | export default 'WORLD!'; 4 | 5 | Debug('dep2')('world!'); 6 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/error.js: -------------------------------------------------------------------------------- 1 | setTimeout(() => { 2 | throw Error('ooops!'); 3 | }, 50); 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/font.woff -------------------------------------------------------------------------------- /test/unit/fixtures/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | 10 | Test 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/module-with-deps.js: -------------------------------------------------------------------------------- 1 | import hi from './dep-esm.js'; 2 | import Debug from 'debug'; 3 | 4 | const debug = new Debug('test'); 5 | 6 | debug(hi); 7 | 8 | export { hi as default }; 9 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/module.js: -------------------------------------------------------------------------------- 1 | import dep from './dep-esm.js'; 2 | 3 | function main() { 4 | console.log(dep); 5 | } 6 | 7 | main(); 8 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/nested-ts/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/nested-ts/index.ts -------------------------------------------------------------------------------- /test/unit/fixtures/www/nested/foo.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/nested/foo.jsx -------------------------------------------------------------------------------- /test/unit/fixtures/www/nested/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Nested 5 | 6 | 7 | Test Nested 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/nested/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/nested/index.js -------------------------------------------------------------------------------- /test/unit/fixtures/www/nested/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } -------------------------------------------------------------------------------- /test/unit/fixtures/www/otherstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/script.js: -------------------------------------------------------------------------------- 1 | console.log('script'); 2 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/spå ces.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/spå ces.js -------------------------------------------------------------------------------- /test/unit/fixtures/www/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "test" 3 | } 4 | -------------------------------------------------------------------------------- /test/unit/fixtures/www/ts.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popeindustries/dvlp/f4a6f2682a57257951d46cddd48c2c22d7ba671a/test/unit/fixtures/www/ts.ts -------------------------------------------------------------------------------- /test/unit/hooks-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import fs from 'node:fs'; 3 | import { getBundleFilePath } from './utils.js'; 4 | import { Hooker } from '../../src/hooks/index.js'; 5 | import hooksFixture from './fixtures/hooks-bundle.mjs'; 6 | import { init } from 'cjs-module-lexer'; 7 | import path from 'node:path'; 8 | import transformBundleFixture from './fixtures/hooks-transform-bundle.mjs'; 9 | import transformFixture from './fixtures/hooks-transform.mjs'; 10 | 11 | function getResponse() { 12 | return { 13 | end(body) { 14 | this.body = body; 15 | this.finished = true; 16 | }, 17 | metrics: { 18 | getEvent() { 19 | return 0; 20 | }, 21 | recordEvent() {}, 22 | }, 23 | writeHead() {}, 24 | }; 25 | } 26 | 27 | /** @type { Hooks } */ 28 | let hooks; 29 | 30 | describe('hooks()', () => { 31 | before(async () => { 32 | await init(); 33 | }); 34 | 35 | describe('bundle', () => { 36 | afterEach(() => { 37 | hooks && hooks.destroy(); 38 | }); 39 | 40 | it('should return "undefined" if no module bundle found', async () => { 41 | const hooks = new Hooker(); 42 | expect( 43 | await hooks.bundleDependency('./dvlp/bundle-xxx/foofoo-0.0.0.js'), 44 | ).to.equal(undefined); 45 | }); 46 | it('should bundle filePath', async () => { 47 | const hooks = new Hooker(); 48 | const filePath = path.resolve(getBundleFilePath('debug')); 49 | await hooks.bundleDependency(filePath, getResponse()); 50 | const module = fs.readFileSync(filePath, 'utf8'); 51 | expect(module).to.include('export_default as default'); 52 | }); 53 | it('should bundle and add missing named exports', async () => { 54 | const hooks = new Hooker(); 55 | const filePath = path.resolve(getBundleFilePath('react')); 56 | await hooks.bundleDependency(filePath, getResponse()); 57 | const module = fs.readFileSync(filePath, 'utf8'); 58 | expect(module).to.include('export_default as default'); 59 | expect(module).to.include('export_Children as Children'); 60 | }); 61 | it('should return cached bundle', async () => { 62 | const hooks = new Hooker(); 63 | const filePath = path.resolve(getBundleFilePath('debug')); 64 | fs.writeFileSync(filePath, 'this is cached'); 65 | await hooks.bundleDependency(filePath, getResponse()); 66 | const module = fs.readFileSync(filePath, 'utf8'); 67 | expect(module).to.equal('this is cached'); 68 | fs.unlinkSync(filePath); 69 | }); 70 | it('should bundle with custom hook', async () => { 71 | const hooks = new Hooker(hooksFixture); 72 | const filePath = path.resolve(getBundleFilePath('debug')); 73 | await hooks.bundleDependency(filePath, getResponse()); 74 | const module = fs.readFileSync(filePath, 'utf8'); 75 | expect(module).to.contain('this is bundled content for: browser.js'); 76 | }); 77 | }); 78 | 79 | describe('transform', () => { 80 | it('should transform filePath', async () => { 81 | const hooks = new Hooker(); 82 | const filePath = path.resolve('./test/unit/fixtures/www/dep.ts'); 83 | const res = getResponse(); 84 | await hooks.transform(filePath, '', res, { 85 | client: { ua: 'test' }, 86 | }); 87 | expect(res.body).to.contain('dep_default as default'); 88 | }); 89 | it('should transform with custom hook', async () => { 90 | const hooks = new Hooker(transformFixture); 91 | const filePath = path.resolve('./test/unit/fixtures/www/script.js'); 92 | const res = getResponse(); 93 | await hooks.transform(filePath, '', res, { 94 | client: { ua: 'test' }, 95 | }); 96 | expect(res.body).to.contain('this is transformed content for: script.js'); 97 | }); 98 | it('should add project dependencies to optional watcher', async () => { 99 | const added = []; 100 | const hooks = new Hooker(transformBundleFixture, { 101 | add(filePath) { 102 | added.push(filePath); 103 | }, 104 | }); 105 | const filePath = path.resolve( 106 | './test/unit/fixtures/www/module-with-deps.js', 107 | ); 108 | const res = getResponse(); 109 | await hooks.transform(filePath, '', res, { 110 | client: { ua: 'test' }, 111 | }); 112 | expect( 113 | added.includes(path.resolve('./test/unit/fixtures/www/dep-esm.js')), 114 | ).to.be.true; 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import './file-test.js'; 2 | import './hooks-test.js'; 3 | import './intercept-test.js'; 4 | import './platform-test.js'; 5 | import './patch-test.js'; 6 | import './resolver-test.js'; 7 | import './mock-test.js'; 8 | import './test-server-test.js'; 9 | import './server-test.js'; 10 | import './electron-test.js'; 11 | -------------------------------------------------------------------------------- /test/unit/init.js: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '../../src/utils/bootstrap.js'; 2 | 3 | bootstrap(); 4 | -------------------------------------------------------------------------------- /test/unit/intercept-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import http from 'node:http'; 3 | import { interceptClientRequest } from '../../src/utils/intercept-client-request.js'; 4 | import { testServer } from '../../src/dvlp-test.js'; 5 | 6 | /** @type { () => void } */ 7 | let unintercept; 8 | let server; 9 | 10 | describe('intercept', () => { 11 | afterEach(async () => { 12 | unintercept?.(); 13 | }); 14 | 15 | describe('intercept-client-request', () => { 16 | beforeEach(async () => { 17 | server = await testServer({ autorespond: true, latency: 0 }); 18 | }); 19 | afterEach(async () => { 20 | server && (await server.destroy()); 21 | }); 22 | 23 | describe('fetch', () => { 24 | it('should intercept "fetch(string)"', (done) => { 25 | unintercept = interceptClientRequest((url) => { 26 | expect(url).to.have.property('href', 'http://localhost:8080/test'); 27 | done(); 28 | }); 29 | fetch('http://localhost:8080/test'); 30 | }); 31 | it('should intercept "fetch(string)" with modification', async () => { 32 | unintercept = interceptClientRequest((url) => { 33 | url.searchParams.set('intercepted', 'true'); 34 | return true; 35 | }); 36 | const res = await fetch('http://localhost:8080/test'); 37 | expect(await res.text()).to.equal( 38 | '"hello from http://localhost:8080/test?intercepted=true!"', 39 | ); 40 | }); 41 | it('should intercept "fetch(string, options)" with modification', async () => { 42 | unintercept = interceptClientRequest((url) => { 43 | url.searchParams.set('intercepted', 'true'); 44 | return true; 45 | }); 46 | const res = await fetch('http://localhost:8080/test', { 47 | headers: { 'x-test': 'true' }, 48 | }); 49 | expect(res.headers.get('x-test')).to.equal('true'); 50 | expect(await res.text()).to.equal( 51 | '"hello from http://localhost:8080/test?intercepted=true!"', 52 | ); 53 | }); 54 | it('should intercept "fetch(request)"', (done) => { 55 | unintercept = interceptClientRequest((url) => { 56 | expect(url).to.have.property('href', 'http://localhost:8080/test'); 57 | done(); 58 | }); 59 | fetch(new Request('http://localhost:8080/test')); 60 | }); 61 | it('should intercept "fetch(request)" with modification', async () => { 62 | unintercept = interceptClientRequest((url) => { 63 | url.searchParams.set('intercepted', 'true'); 64 | return true; 65 | }); 66 | const res = await fetch(new Request('http://localhost:8080/test')); 67 | expect(await res.text()).to.equal( 68 | '"hello from http://localhost:8080/test?intercepted=true!"', 69 | ); 70 | }); 71 | it('should intercept "fetch(request, options)" with modification', async () => { 72 | unintercept = interceptClientRequest((url) => { 73 | url.searchParams.set('intercepted', 'true'); 74 | return true; 75 | }); 76 | const res = await fetch( 77 | new Request('http://localhost:8080/test', { 78 | headers: { 'x-test': 'true' }, 79 | }), 80 | ); 81 | expect(res.headers.get('x-test')).to.equal('true'); 82 | expect(await res.text()).to.equal( 83 | '"hello from http://localhost:8080/test?intercepted=true!"', 84 | ); 85 | }); 86 | }); 87 | 88 | describe('http.get', () => { 89 | it('should intercept "get(string)"', (done) => { 90 | unintercept = interceptClientRequest((url) => { 91 | expect(url).to.have.property('href', 'http://localhost:8080/test'); 92 | done(); 93 | }); 94 | httpGetToPromise('http://localhost:8080/test'); 95 | }); 96 | it('should intercept "get(string)" with modification', async () => { 97 | unintercept = interceptClientRequest((url) => { 98 | url.searchParams.set('intercepted', 'true'); 99 | return true; 100 | }); 101 | const res = await httpGetToPromise('http://localhost:8080/test'); 102 | expect(res).to.equal( 103 | '"hello from http://localhost:8080/test?intercepted=true!"', 104 | ); 105 | }); 106 | it('should intercept "get(options)" with modification', async () => { 107 | unintercept = interceptClientRequest((url) => { 108 | url.searchParams.set('intercepted', 'true'); 109 | return true; 110 | }); 111 | const res = await httpGetToPromise({ 112 | href: 'http://localhost:8080/test', 113 | }); 114 | expect(res).to.equal( 115 | '"hello from http://localhost:8080/test?intercepted=true!"', 116 | ); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | function httpGetToPromise(...args) { 123 | return new Promise((resolve, reject) => { 124 | http 125 | .get(...args, (res) => { 126 | let data = ''; 127 | res.on('data', (chunk) => (data += chunk)); 128 | res.on('end', () => resolve(data)); 129 | }) 130 | .on('error', reject); 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /test/unit/platform-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | parseEsbuildTarget, 3 | parseUserAgent, 4 | } from '../../src/utils/platform.js'; 5 | import { expect } from 'chai'; 6 | 7 | describe('platform', () => { 8 | describe('parseEsbuildTarget', () => { 9 | it('should return default for missing ua', () => { 10 | expect(parseEsbuildTarget(parseUserAgent())).to.equal('es2020'); 11 | }); 12 | it('should return default for unknown ua', () => { 13 | expect(parseEsbuildTarget(parseUserAgent('xxxxxxxxx'))).to.equal( 14 | 'es2020', 15 | ); 16 | }); 17 | it('should correctly parse mobile chrome ua', () => { 18 | expect( 19 | parseEsbuildTarget( 20 | parseUserAgent( 21 | 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36', 22 | ), 23 | ), 24 | ).to.equal('chrome87'); 25 | }); 26 | it('should correctly parse ua with missing browser name', () => { 27 | expect( 28 | parseEsbuildTarget( 29 | parseUserAgent( 30 | 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 5.5) AppleWebKit/537.36 (KHTML, like Gecko) 69.0.3497.106/5.5 TV Safari/537.36', 31 | ), 32 | ), 33 | ).to.equal('chrome69'); 34 | }); 35 | it('should correctly parse ios ua', () => { 36 | expect( 37 | parseEsbuildTarget( 38 | parseUserAgent( 39 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1', 40 | ), 41 | ), 42 | ).to.equal('ios13'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/utils.js: -------------------------------------------------------------------------------- 1 | import { getBundlePath } from '../../src/utils/bundling.js'; 2 | import { resolve } from '../../src/resolver/index.js'; 3 | 4 | export function getBundleFilePath(specifier) { 5 | return getBundlePath(specifier, resolve(specifier)); 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "lib": ["esnext", "DOM"], 10 | "module": "NodeNext", 11 | "moduleResolution": "NodeNext", 12 | "noEmit": true, 13 | "strict": true, 14 | "target": "esnext" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules", "test"] 18 | } 19 | --------------------------------------------------------------------------------