From b17ddabec708da1b32d5377f7eefc107c148dfbe Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 10:40:39 +0000 Subject: [PATCH 01/18] added env variable EVIDENCE_ALLOWED_HOST --- packages/evidence/scripts/build-template.js | 188 ++++++++++---------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/packages/evidence/scripts/build-template.js b/packages/evidence/scripts/build-template.js index cc3b6caddd..ec8f69cc91 100644 --- a/packages/evidence/scripts/build-template.js +++ b/packages/evidence/scripts/build-template.js @@ -45,107 +45,109 @@ const configFileLocation = new URL('svelte.config.js', import.meta.url); fs.writeFileSync('./template/svelte.config.js', fs.readFileSync(configFileLocation)); fsExtra.outputFileSync( - './template/vite.config.js', - `import { sveltekit } from "@sveltejs/kit/vite" - import { createLogger } from 'vite'; - import { sourceQueryHmr, configVirtual, queryDirectoryHmr } from '@evidence-dev/sdk/build/vite'; - import { isDebug } from '@evidence-dev/sdk/utils'; - import { log } from "@evidence-dev/sdk/logger"; - import { evidenceThemes } from '@evidence-dev/tailwind/vite-plugin'; - import tailwindcss from '@tailwindcss/vite'; - - - process.removeAllListeners('warning'); - process.on('warning', (warning) => { - if (warning.name === 'ExperimentalWarning' && - warning.message.includes('CommonJS module') && - warning.message.includes('ES Module')) { - return; - } - console.warn(warning); - }); + './template/vite.config.js', ` +import { sveltekit } from "@sveltejs/kit/vite" +import { createLogger } from 'vite'; +import { sourceQueryHmr, configVirtual, queryDirectoryHmr } from '@evidence-dev/sdk/build/vite'; +import { isDebug } from '@evidence-dev/sdk/utils'; +import { log } from "@evidence-dev/sdk/logger"; +import { evidenceThemes } from '@evidence-dev/tailwind/vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + + +process.removeAllListeners('warning'); +process.on('warning', (warning) => { + if (warning.name === 'ExperimentalWarning' && + warning.message.includes('CommonJS module') && + warning.message.includes('ES Module')) { + return; + } + console.warn(warning); +}); - const logger = createLogger(); - - const strictFs = (process.env.NODE_ENV === 'development') ? false : true; - /** @type {import('vite').UserConfig} */ - const config = - { - plugins: [tailwindcss(), sveltekit(), configVirtual(), queryDirectoryHmr, sourceQueryHmr(), evidenceThemes()], - optimizeDeps: { - include: ['echarts-stat', 'echarts', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', - // We need these to prevent HMR from doing a full page reload - ...(process.env.EVIDENCE_DISABLE_INCLUDE ? [] : [ - '@evidence-dev/core-components', - // Evidence packages injected into process-queries - ${preprocess.injectedEvidenceImports.map((i) => `'${i.from}'`).join(',')}, - 'debounce', - '@duckdb/duckdb-wasm', - 'apache-arrow' - ]) - - ], - exclude: ['svelte-icons', '@evidence-dev/universal-sql', '$evidence/config', '$evidence/themes'] - }, - ssr: { - external: ['@evidence-dev/telemetry', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', '@evidence-dev/sdk/plugins'] - }, - server: { - fs: { - strict: strictFs // allow template to get dependencies outside the .evidence folder - }, - hmr: { - overlay: false - } - }, - build: { - // 🚩 Triple check this - minify: isDebug() ? false : true, - target: isDebug() ? 'esnext' : undefined, - rollupOptions: { - external: [/^@evidence-dev\\/tailwind\\/fonts\\//], - onwarn(warning, warn) { - if (warning.code === 'EVAL') return; - warn(warning); - } - } +const logger = createLogger(); + +const strictFs = (process.env.NODE_ENV === 'development') ? false : true; +/** @type {import('vite').UserConfig} */ +const config = +{ + plugins: [tailwindcss(), sveltekit(), configVirtual(), queryDirectoryHmr, sourceQueryHmr(), evidenceThemes()], + optimizeDeps: { + include: ['echarts-stat', 'echarts', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', + // We need these to prevent HMR from doing a full page reload + ...(process.env.EVIDENCE_DISABLE_INCLUDE ? [] : [ + '@evidence-dev/core-components', +${ // Evidence packages injected into process-queries + preprocess.injectedEvidenceImports.map((i) => `\t\t\t\t'${i.from}'`).join(',\n')}, + 'debounce', + '@duckdb/duckdb-wasm', + 'apache-arrow' + ]) + ], + exclude: ['svelte-icons', '@evidence-dev/universal-sql', '$evidence/config', '$evidence/themes'] + }, + ssr: { + external: ['@evidence-dev/telemetry', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', '@evidence-dev/sdk/plugins'] + }, + server: { + fs: { + strict: strictFs // allow template to get dependencies outside the .evidence folder }, - customLogger: logger - } - - // Suppress errors when building in non-debug mode - if (!isDebug() && process.env.EVIDENCE_IS_BUILDING === 'true') { - config.logLevel = 'silent'; - logger.error = (msg) => log.error(msg); - logger.info = () => {}; - logger.warn = () => {}; - logger.warnOnce = () => {}; - } else { - const loggerWarn = logger.warn; - const loggerOnce = logger.warnOnce - - /** - * @see https://github.com/evidence-dev/evidence/issues/1876 - * Ignore the duckdb-wasm sourcemap warning - */ - logger.warnOnce = (m, o) => { - if (m.match(/Sourcemap for ".+\\/node_modules\\/@duckdb\\/duckdb-wasm\\/dist\\/duckdb-browser-eh\\.worker\\.js" points to missing source files/)) return; - loggerOnce(m, o) + hmr: { + overlay: false + }, + allowedHosts: [ + ...(process.env.EVIDENCE_ALLOWED_HOST ? [process.env.EVIDENCE_ALLOWED_HOST] : []) + ] + }, + build: { + // 🚩 Triple check this + minify: isDebug() ? false : true, + target: isDebug() ? 'esnext' : undefined, + rollupOptions: { + external: [/^@evidence-dev\\/tailwind\\/fonts\\//], + onwarn(warning, warn) { + if (warning.code === 'EVAL') return; + warn(warning); + } } + }, + customLogger: logger +} + +// Suppress errors when building in non-debug mode +if (!isDebug() && process.env.EVIDENCE_IS_BUILDING === 'true') { + config.logLevel = 'silent'; + logger.error = (msg) => log.error(msg); + logger.info = () => {}; + logger.warn = () => {}; + logger.warnOnce = () => {}; +} else { + const loggerWarn = logger.warn; + const loggerOnce = logger.warnOnce + + /** + * @see https://github.com/evidence-dev/evidence/issues/1876 + * Ignore the duckdb-wasm sourcemap warning + */ + logger.warnOnce = (m, o) => { + if (m.match(/Sourcemap for ".+\\/node_modules\\/@duckdb\\/duckdb-wasm\\/dist\\/duckdb-browser-eh\\.worker\\.js" points to missing source files/)) return; + loggerOnce(m, o) + } - logger.warn = (msg, options) => { - // ignore fs/promises warning, used in +layout.js behind if (!browser) check - if (msg.includes('Module "fs/promises" has been externalized for browser compatibility')) return; + logger.warn = (msg, options) => { + // ignore fs/promises warning, used in +layout.js behind if (!browser) check + if (msg.includes('Module "fs/promises" has been externalized for browser compatibility')) return; - // ignore eval warning, used in duckdb-wasm - if (msg.includes('Use of eval in') && msg.includes('is strongly discouraged as it poses security risks and may cause issues with minification.')) return; + // ignore eval warning, used in duckdb-wasm + if (msg.includes('Use of eval in') && msg.includes('is strongly discouraged as it poses security risks and may cause issues with minification.')) return; - loggerWarn(msg, options); - }; - } + loggerWarn(msg, options); + }; +} - export default config` +export default config` ); // Create a readme From 841fc5b605be4f45ba8891bcffed9eadbfa3b79d Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 10:41:42 +0000 Subject: [PATCH 02/18] added a --disable-watchers cli option --- packages/evidence/cli.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/evidence/cli.js b/packages/evidence/cli.js index 0832c129bd..2a796b9609 100755 --- a/packages/evidence/cli.js +++ b/packages/evidence/cli.js @@ -60,7 +60,7 @@ const runFileWatcher = function (watchPatterns) { var watchers = []; - watchPatterns.forEach((pattern, item) => { + watchPatterns.filter(p => p?.disabled !== true).forEach((pattern, item) => { watchers[item] = chokidar.watch(path.join(pattern.sourceRelative, pattern.filePattern), { ignored: ignoredFiles }); @@ -131,11 +131,13 @@ const watchPatterns = [ filePattern: '**' }, // static files (eg images) { + mnemonic: 'sources', sourceRelative: './sources/', targetRelative: './.evidence/template/sources/', filePattern: '**' }, // source files (eg csv files) { + mnemonic: 'queries', sourceRelative: './queries', targetRelative: './.evidence/template/queries', filePattern: '**' @@ -145,7 +147,11 @@ const watchPatterns = [ targetRelative: './.evidence/template/src/components/', filePattern: '**' }, // custom components - { sourceRelative: '.', targetRelative: './.evidence/template/src/', filePattern: 'app.css' }, // custom theme file + { + sourceRelative: '.', + targetRelative: './.evidence/template/src/', + filePattern: 'app.css' + }, // custom theme file { sourceRelative: './partials', targetRelative: './.evidence/template/partials', @@ -161,6 +167,7 @@ function removeStaticDir(dir) { const strictMode = function () { enableStrictMode(); }; + const buildHelper = function (command, args) { const watchers = runFileWatcher(watchPatterns); const flatArgs = flattenArguments(args); @@ -177,7 +184,7 @@ const buildHelper = function (command, args) { // used for source query HMR EVIDENCE_DATA_URL_PREFIX: process.env.EVIDENCE_DATA_URL_PREFIX ?? 'static/data', EVIDENCE_DATA_DIR: process.env.EVIDENCE_DATA_DIR ?? './static/data', - EVIDENCE_IS_BUILDING: 'true' + EVIDENCE_IS_BUILDING: 'true', } }); // Copy the outputs to the root of the project upon successful exit @@ -234,6 +241,7 @@ const prog = sade('evidence'); prog .command('dev') + .option('--disable-watchers', 'Disables watching certain directories [sources,queries]') .option('--debug', 'Enables verbose console logs') .describe('launch the local evidence development environment') .action((args) => { @@ -243,6 +251,23 @@ prog delete args.debug; } + if (args['disable-watchers']) { + const directories = args['disable-watchers'].split(/\W/).filter(v => v.length > 0); + const mnemonics = watchPatterns.filter(p => Object.hasOwn(p, 'mnemonic')).map(p => p.mnemonic); + if (directories.some(v => !mnemonics.includes(v))) { + console.warn(chalk.yellow(`Unknown watch directory/directories: "${args['disable-watchers']}"`)); + console.info(`Directories for which watching can be disabled are: ${mnemonics.join(', ') }`); + process.exit(1); + } + + watchPatterns.forEach(p => { + p.disabled = Object.hasOwn(p, 'mnemonic') && directories.includes(p.mnemonic); + }); + + console.info(chalk.bold(`These directories will not be watched: ${directories.join(', ')}`)); + delete args['disable-watchers']; + } + loadEnvFile(); const manifestExists = fs.lstatSync( @@ -280,7 +305,7 @@ ${chalk.bold('[!] Unable to load source manifest')} ...process.env, // used for source query HMR EVIDENCE_DATA_URL_PREFIX: process.env.EVIDENCE_DATA_URL_PREFIX ?? 'static/data', - EVIDENCE_DATA_DIR: process.env.EVIDENCE_DATA_DIR ?? './static/data' + EVIDENCE_DATA_DIR: process.env.EVIDENCE_DATA_DIR ?? './static/data', } }); From e4760592b6504df2d18a3f210e89ccb83507b02f Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 12:03:58 +0000 Subject: [PATCH 03/18] fix(tailwind): add content paths for local pkg dev Add relative paths to Tailwind content scanning to support local Evidence package development using the file: protocol. Maintains full compatibility with published packages. (according to Claude) --- packages/ui/tailwind/src/config/config.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/ui/tailwind/src/config/config.js b/packages/ui/tailwind/src/config/config.js index afb05e8308..b87da703c0 100644 --- a/packages/ui/tailwind/src/config/config.js +++ b/packages/ui/tailwind/src/config/config.js @@ -9,6 +9,19 @@ const themes = buildThemes(themesConfig); /** @type {Partial} */ export const config = { + content: [ + // Core Evidence components source files (for local development) + '../../../core-components/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/core-components/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/core-components/dist/**/*.{html,js,svelte,ts,md}', + // Additional Evidence UI packages + '../../../*/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/*/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/*/dist/**/*.{html,js,svelte,ts,md}', + // Consumer project files + './src/**/*.{html,js,svelte,ts,md}', + './.evidence/template/src/**/*.{html,js,svelte,ts,md}' + ], theme: { extend: { fontFamily: { From 5aa82cf17099093775bc98b6535d8f7c13b6ca1c Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 18:58:48 +0000 Subject: [PATCH 04/18] added a --disable-hmr cli option disabling the watchers for sources and queries was not enough to prevent these from reloading --- packages/evidence/cli.js | 13 ++++- packages/evidence/scripts/build-template.js | 11 +++- packages/lib/sdk/src/lib/hmr.js | 63 +++++++++++++++++++++ packages/lib/sdk/src/utils/index.js | 1 + 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 packages/lib/sdk/src/lib/hmr.js diff --git a/packages/evidence/cli.js b/packages/evidence/cli.js index 2a796b9609..4e61dc829d 100755 --- a/packages/evidence/cli.js +++ b/packages/evidence/cli.js @@ -8,7 +8,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import sade from 'sade'; import { logQueryEvent } from '@evidence-dev/telemetry'; -import { enableDebug, enableStrictMode } from '@evidence-dev/sdk/utils'; +import { enableDebug, enableStrictMode, disableHmr } from '@evidence-dev/sdk/utils'; import { loadEnv } from 'vite'; import { createHash } from 'crypto'; @@ -241,6 +241,7 @@ const prog = sade('evidence'); prog .command('dev') + .option('--disable-hmr', 'Disables hot-reloading of certain features [sources,queries]') .option('--disable-watchers', 'Disables watching certain directories [sources,queries]') .option('--debug', 'Enables verbose console logs') .describe('launch the local evidence development environment') @@ -268,6 +269,16 @@ prog delete args['disable-watchers']; } + if (args['disable-hmr']) { + try { + disableHmr(args['disable-hmr'].split(/\W/).filter(v => v.length > 0)); + } catch (_) { + console.warn(chalk.yellow(`Unknown HMR feature(s): "${args['disable-hmr']}"`)); + } + + delete args['disable-hmr']; + } + loadEnvFile(); const manifestExists = fs.lstatSync( diff --git a/packages/evidence/scripts/build-template.js b/packages/evidence/scripts/build-template.js index ec8f69cc91..df7403329e 100644 --- a/packages/evidence/scripts/build-template.js +++ b/packages/evidence/scripts/build-template.js @@ -49,7 +49,7 @@ fsExtra.outputFileSync( import { sveltekit } from "@sveltejs/kit/vite" import { createLogger } from 'vite'; import { sourceQueryHmr, configVirtual, queryDirectoryHmr } from '@evidence-dev/sdk/build/vite'; -import { isDebug } from '@evidence-dev/sdk/utils'; +import { isDebug, isHmrEnabled } from '@evidence-dev/sdk/utils'; import { log } from "@evidence-dev/sdk/logger"; import { evidenceThemes } from '@evidence-dev/tailwind/vite-plugin'; import tailwindcss from '@tailwindcss/vite'; @@ -72,7 +72,14 @@ const strictFs = (process.env.NODE_ENV === 'development') ? false : true; /** @type {import('vite').UserConfig} */ const config = { - plugins: [tailwindcss(), sveltekit(), configVirtual(), queryDirectoryHmr, sourceQueryHmr(), evidenceThemes()], + plugins: [ + tailwindcss(), + sveltekit(), + configVirtual(), + ...(isHmrEnabled('queries') ? [queryDirectoryHmr] : []), + ...(isHmrEnabled('sources') ? [sourceQueryHmr()] : []), + evidenceThemes() + ], optimizeDeps: { include: ['echarts-stat', 'echarts', 'blueimp-md5', 'nanoid', '@uwdata/mosaic-sql', // We need these to prevent HMR from doing a full page reload diff --git a/packages/lib/sdk/src/lib/hmr.js b/packages/lib/sdk/src/lib/hmr.js new file mode 100644 index 0000000000..acfa888ecb --- /dev/null +++ b/packages/lib/sdk/src/lib/hmr.js @@ -0,0 +1,63 @@ +/// + +import chalk from 'chalk'; + +/** @param {unknown} directory + * @returns {('sources' | 'queries')[]} */ +const validateHmrDirectory = (directory) => { + const validValues = ['sources', 'queries']; + + if (typeof directory === 'string') + directory = [directory]; + + if (Array.isArray(directory)) { + const uniqueDirectories = [...new Set(directory).values()]; + if (uniqueDirectories.every(v => typeof v === 'string' + && validValues.includes(v))) return uniqueDirectories; + } + + throw new Error('HMR can only be disabled/enabled for "sources", "queries"'); +}; + +/** @param {('sources' | 'queries') | ('sources' | 'queries')[]} directory */ +export const disableHmr = (directory) => { + if (typeof process === 'undefined') { + throw new Error('HMR can only be disabled on the server side'); + } + + directory = validateHmrDirectory(directory); + console.info(chalk.bold.yellow( + `Evidence will not re-build the sources or hydrate the application automatically ` + + `when files within the ${directory.join('/')} folder(s) are changed.` + )); + directory.forEach(v => { + process.env[`EVIDENCE_DISABLE_${v.toUpperCase()}_HMR`] = 'true'; + }); +} + +/** @param {('sources' | 'queries') | ('sources' | 'queries')[]} directory */ +export const enableHmr = (directory) => { + if (typeof process === 'undefined') { + throw new Error('HMR can only be enabled on the server side'); + } + + directory = validateHmrDirectory(directory); + directory.forEach(v => { + delete process.env[`EVIDENCE_DISABLE_${v.toUpperCase()}_HMR`]; + }); +} + +/** @param {('sources' | 'queries') | ('sources' | 'queries')[]} directory + * @returns {boolean | undefined} */ +export const isHmrEnabled = (directory) => { + directory = validateHmrDirectory(directory); + /** @param {string} v */ + const envName = (v) => `EVIDENCE_DISABLE_${v.toUpperCase()}_HMR`; + + if (typeof process !== 'undefined') + return directory.every(v => + process.env[envName(v)] !== 'true'); + if (typeof import.meta.env !== 'undefined') + return directory.every(v => + import.meta.env[envName(v)] !== 'true'); +} \ No newline at end of file diff --git a/packages/lib/sdk/src/utils/index.js b/packages/lib/sdk/src/utils/index.js index 582da2078b..2207ec724f 100644 --- a/packages/lib/sdk/src/utils/index.js +++ b/packages/lib/sdk/src/utils/index.js @@ -5,3 +5,4 @@ export * from '../lib/history/index.js'; export * from '../lib/debug.js'; export * from './browserDebounce.js'; export * from '../lib/strict.js'; +export * from '../lib/hmr.js'; From 6f5ad7af1f92b001d12ed41dad270beeeaa94c23 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 20:12:20 +0000 Subject: [PATCH 05/18] added env variable EVIDENCE_BASE_PATH This allows me to use code-server's automatic port forwarding proxy which serves the dev website at: https://hostname/proxy/port by setting the env var to '/proxy/port' and in case of using a cloudflare tunnel, configuring the tunnel to use the same path --- packages/evidence/scripts/build-template.js | 12 ++++++------ .../src/configuration/schemas/deployment.schema.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/evidence/scripts/build-template.js b/packages/evidence/scripts/build-template.js index df7403329e..42facae7a8 100644 --- a/packages/evidence/scripts/build-template.js +++ b/packages/evidence/scripts/build-template.js @@ -70,14 +70,14 @@ const logger = createLogger(); const strictFs = (process.env.NODE_ENV === 'development') ? false : true; /** @type {import('vite').UserConfig} */ -const config = +const config = { plugins: [ - tailwindcss(), - sveltekit(), - configVirtual(), - ...(isHmrEnabled('queries') ? [queryDirectoryHmr] : []), - ...(isHmrEnabled('sources') ? [sourceQueryHmr()] : []), + tailwindcss(), + sveltekit(), + configVirtual(), + ...(isHmrEnabled('queries') ? [queryDirectoryHmr] : []), + ...(isHmrEnabled('sources') ? [sourceQueryHmr()] : []), evidenceThemes() ], optimizeDeps: { diff --git a/packages/lib/sdk/src/configuration/schemas/deployment.schema.js b/packages/lib/sdk/src/configuration/schemas/deployment.schema.js index 0df63d57f3..c74c778973 100644 --- a/packages/lib/sdk/src/configuration/schemas/deployment.schema.js +++ b/packages/lib/sdk/src/configuration/schemas/deployment.schema.js @@ -4,6 +4,6 @@ export const DeploymentConfigSchema = z.object({ basePath: z .string() .optional() - .default('') + .default(process.env.EVIDENCE_BASE_PATH || '') .refine((value) => !value || value.startsWith('/'), { message: 'basePath must start with /' }) }); From af238ca74fe74ecef353e8dbd7db6563ac8e7f28 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 20:22:51 +0000 Subject: [PATCH 06/18] added documentation of another dev workflow I used --- CONTRIBUTING.md | 17 ++++++ DEVELOP.md | 143 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 DEVELOP.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5fb58620c..993b8acec4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,13 @@ To request a feature, a new data source, or ask for help, create a GitHub discus ### Getting Started +Evidence development can be done in two ways: + +1. **Repository Development** (recommended): Work directly in the Evidence repository using the example project +2. **Local Package Linking**: Link your existing Evidence project to local Evidence packages + +#### Repository Development + Follow these steps to test your changes, once you've started the example project (per steps below), you should be able to open the `Evidence Development Workspace` on `localhost:3000`. Any subsequent changes you make will be reflected on the website. @@ -75,6 +82,16 @@ pnpm run dev:core-components pnpm run dev:example-project ``` +#### Local Package Linking + +If you prefer to develop using your existing Evidence project instead of the example project: + +1. Clone and build the Evidence repository +2. Link local packages in your project's `package.json` +3. Configure Tailwind CSS and Vite for local development + +For detailed setup instructions, see [DEVELOP.md](./DEVELOP.md). + #### Cannot find package Error If you get: `Error [ERR_MODULE_NOT_FOUND]: Cannot find package [...]`. You might need to clean the `caches` diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000000..8e9c8885a3 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,143 @@ +# Development Setup with Local Evidence Packages + +This guide explains how to set up an Evidence project to use local Evidence packages for development, allowing you to edit Evidence source code and see changes immediately. + +## Prerequisites + +- An existing Evidence project +- Local clone of the Evidence repository +- Node.js and npm installed + +## Step 1: Clone Evidence Repository + +```bash +git clone https://github.com/evidence-dev/evidence.git +cd evidence +git checkout release-2025-02-20 # Use stable release branch +npm install +npm run build +``` + +## Step 2: Configure Your Evidence Project + +In your Evidence project's `package.json`, replace Evidence package references with local file paths: + +```json +{ + "dependencies": { + "@evidence-dev/core-components": "file:../evidence/packages/ui/core-components", + "@evidence-dev/component-utilities": "file:../evidence/packages/lib/component-utilities", + "@evidence-dev/sdk": "file:../evidence/packages/lib/sdk", + "@evidence-dev/db-commons": "file:../evidence/packages/lib/db-commons", + "@evidence-dev/universal-sql": "file:../evidence/packages/lib/universal-sql", + "@evidence-dev/tailwind": "file:../evidence/packages/ui/tailwind" + }, + "devDependencies": { + "@sveltejs/adapter-static": "3.0.1", + "@sveltejs/kit": "2.8.4", + "@sveltejs/vite-plugin-svelte": "3.1.2", + "@tailwindcss/vite": "^4.0.0", + "svelte": "4.2.19", + "vite": "5.4.14" + } +} +``` + +**Important**: Use exact versions to match Evidence's dependencies, especially Svelte 4.2.19. + +## Step 3: Fix Tailwind CSS Content Scanning + +When using local packages, Tailwind CSS v4 cannot find Evidence component files. You need to update the Evidence Tailwind configuration: + +Edit `/path/to/evidence/packages/ui/tailwind/src/config/config.js`: + +```javascript +export const config = { + content: [ + // Core Evidence components source files (for local development) + '../../../core-components/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/core-components/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/core-components/dist/**/*.{html,js,svelte,ts,md}', + + // Additional Evidence UI packages + '../../../*/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/*/src/**/*.{html,js,svelte,ts,md}', + './node_modules/@evidence-dev/*/dist/**/*.{html,js,svelte,ts,md}', + + // Consumer project files + './src/**/*.{html,js,svelte,ts,md}', + './.evidence/template/src/**/*.{html,js,svelte,ts,md}' + ], + // ... rest of existing config +} +``` + +## Step 4: Update Vite Configuration + +In your project's `vite.config.js`, exclude core-components from optimization: + +```javascript +export default { + // ... other config + optimizeDeps: { + exclude: [ + 'svelte-icons', + '@evidence-dev/universal-sql', + '$evidence/config', + '$evidence/themes', + '@evidence-dev/core-components' // Add this line + ] + } +} +``` + +## Step 5: Install Dependencies and Run + +```bash +# Install with force to handle local package linking +npm install --force + +# Start development server +npm run dev +``` + +## Why This Setup is Needed + +### Svelte Version Compatibility +Evidence is built with Svelte 4.2.19, but newer projects might default to Svelte 5. Version mismatches cause compilation errors. + +### Tailwind CSS Content Scanning +Tailwind CSS v4 scans files to determine which utility classes to generate. When using local packages instead of published npm packages, the file paths change and Tailwind cannot find Evidence component files, resulting in missing CSS rules and broken styling. + +### Vite Dependency Optimization +Vite pre-bundles dependencies for faster loading. Local packages need different handling than published packages, so core-components should be excluded from optimization. + +## Troubleshooting + +### Styling Issues (Oversized Icons, Missing CSS) +This typically indicates Tailwind isn't finding Evidence component files. Verify: +1. The Tailwind config includes the correct content paths +2. The Evidence repository is built (`npm run build` in Evidence repo) +3. File paths in `package.json` are correct relative to your project + +### Build Errors +Check that: +1. Svelte versions match exactly (4.2.19) +2. SvelteKit version is compatible (2.8.4) +3. All local package paths exist and are built + +### Force Reinstall +If packages seem corrupted: +```bash +rm -rf node_modules package-lock.json +npm install --force +``` + +## Development Workflow + +1. Make changes to Evidence source code +2. Build Evidence packages: `cd evidence && npm run postinstall`e +3. Changes should automatically reflect in your project (HMR) +4. For major changes, restart your dev server + +This setup allows you to develop Evidence components locally while maintaining a working project environment. \ No newline at end of file From 94ba13967d23820f6e89f90965ed122cb9f8ac9c Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sat, 20 Sep 2025 20:31:43 +0000 Subject: [PATCH 07/18] added changeset as instructed in contributing doc --- .changeset/stale-lizards-wink.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/stale-lizards-wink.md diff --git a/.changeset/stale-lizards-wink.md b/.changeset/stale-lizards-wink.md new file mode 100644 index 0000000000..37eddff410 --- /dev/null +++ b/.changeset/stale-lizards-wink.md @@ -0,0 +1,7 @@ +--- +'@evidence-dev/evidence': minor +'@evidence-dev/sdk': minor +'@evidence-dev/tailwind': minor +--- + +This PR enhances Evidence's development experience and proxy support with improvements to dev command CLI options and developer workflows. From 7e3f68fe5c76c7cdb71aca7200d95f601de78051 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sun, 21 Sep 2025 12:19:36 +0000 Subject: [PATCH 08/18] explain when dev using linked local pkgs is useful --- DEVELOP.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/DEVELOP.md b/DEVELOP.md index 8e9c8885a3..bfff37550b 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -1,6 +1,19 @@ # Development Setup with Local Evidence Packages -This guide explains how to set up an Evidence project to use local Evidence packages for development, allowing you to edit Evidence source code and see changes immediately. +## When to Use This Guide + +**Most Evidence development should use the standard process:** Work directly in the Evidence repository using the example project, as documented in [CONTRIBUTING.md](./CONTRIBUTING.md). This is simpler, faster, and recommended for most contributors developing Evidence components and framework code. + +**Use this local package linking approach only when:** +- **Developing the Evidence CLI** - Testing template population, file watching, or CLI-specific features +- **End-to-end testing** - Testing the complete Evidence user experience with real project structures +- **Testing with your own Evidence project** - You need to validate changes against your specific setup + +The key difference: the example project uses `vite dev` directly and bypasses Evidence's CLI template system, while real Evidence projects use `evidence dev` which converts Evidence project structure (pages/, sources/, queries/) into SvelteKit structure. + +## Overview + +This guide explains how to link your existing Evidence project to local Evidence packages for development, allowing you to edit Evidence source code and see changes immediately in your own project. ## Prerequisites From 5a889d2c1ecbdbfae103d3717d72a0a4008e5c51 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sun, 21 Sep 2025 18:36:28 +0000 Subject: [PATCH 09/18] save some changes using Claude and start over --- packages/extension/package.json | 35 +++++++++- packages/extension/src/commands/preview.ts | 22 +++++- packages/extension/src/commands/server.ts | 79 ++++++++++++++++------ packages/extension/src/config.ts | 6 +- packages/extension/src/node.ts | 54 +++++++++++++-- packages/extension/src/terminal.ts | 1 + 6 files changed, 166 insertions(+), 31 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index 17fe3b4d41..0618ddf5ba 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -321,12 +321,14 @@ "evidence.autoStart": { "type": "boolean", "default": false, - "description": "Autostart Evidence server when VS Code opens Evidence project workspace." + "description": "Autostart Evidence server when VS Code opens Evidence project workspace.", + "order": 1 }, "evidence.defaultPort": { "type": "number", "default": 3000, - "description": "Default Evidence server port." + "description": "Default Evidence server port.", + "order": 2 }, "evidence.templateProjectUrl": { "type": "string", @@ -348,7 +350,8 @@ "VS Code default browser - split-screen", "Open in external web browser", "No automatic preview shown" - ] + ], + "order": 6 }, "evidence.enableSqlBackground": { "type": "boolean", @@ -393,6 +396,32 @@ ], "default": "lowercase", "description": "Choose whether SQL keyword suggestions should be in uppercase or lowercase" + }, + "evidence.allowedHost": { + "type": "string", + "default": "", + "description": "Hostname for accessing Evidence server (e.g., for code-server or reverse proxy setups). Leave empty for localhost.", + "order": 3 + }, + "evidence.basePath": { + "type": "string", + "default": "", + "description": "Base path for Evidence server URLs (e.g., '/proxy/3710' for code-server auto port forwarding). Must start with '/' if specified.", + "order": 4 + }, + "evidence.disableAutoSourceBuilding": { + "type": "boolean", + "default": false, + "description": "Disable the automatic building of sources by the server. When checked, the server will not automatically rebuild sources or reload when source files change.", + "order": 5 + }, + "evidence.previewDelay": { + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 30, + "description": "Delay in seconds before opening preview after server starts (useful for Cloudflare tunnels or slow proxy setups). Set to 0 for immediate preview.", + "order": 7 } } }, diff --git a/packages/extension/src/commands/preview.ts b/packages/extension/src/commands/preview.ts index d620175e50..51030c1abe 100644 --- a/packages/extension/src/commands/preview.ts +++ b/packages/extension/src/commands/preview.ts @@ -11,7 +11,25 @@ import { getAppPageUri, isServerRunning, startServer } from './server'; import { waitFor } from '../utils/httpUtils'; /** - * Local Evidence app url. + * Gets the base Evidence app URL using configuration settings. + */ +export function getLocalAppUrl(): string { + const allowedHost: string = getConfig(Settings.AllowedHost, '') as string; + const basePath: string = getConfig(Settings.BasePath, '') as string; + + if (allowedHost) { + // Use the configured host (e.g., for code-server or reverse proxy) + const protocol = allowedHost.includes('localhost') ? 'http' : 'https'; + return `${protocol}://${allowedHost}${basePath}`; + } else { + // Default to localhost + return `http://localhost${basePath}`; + } +} + +/** + * Legacy export for backward compatibility. + * @deprecated Use getLocalAppUrl() instead */ export const localAppUrl = `http://localhost`; @@ -62,7 +80,7 @@ export async function preview(uri?: Uri) { } } - if (!isEvidenceProject || !isServerRunning() || /\/pages\/|\\pages\\/.test(uri?.path ?? '')) { + if (!isEvidenceProject || !isServerRunning() || !/\/pages\/|\\pages\\/.test(uri?.path ?? '')) { // show standard markdown document preview commands.executeCommand(Commands.MarkdownShowPreview, uri); return; diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index d954347994..186d2b7d47 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -4,7 +4,7 @@ import { Commands } from './commands'; import { Settings, getConfig } from '../config'; import { getOutputChannel } from '../output'; import { closeTerminal, sendCommand } from '../terminal'; -import { localAppUrl, preview } from './preview'; +import { getLocalAppUrl, preview } from './preview'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from '../node'; import { statusBar } from '../statusBar'; import { timeout } from '../utils/timer'; @@ -35,33 +35,45 @@ const setContext = (key: any, value: any) => { */ export async function getAppPageUri(pageUrl?: string): Promise { const defaultPort = getConfig(Settings.DefaultPort); - const serverUrl = `${localAppUrl}:${defaultPort}`; + const allowedHost = getConfig(Settings.AllowedHost, ''); + + let baseUrl: string; + if (allowedHost) { + // Use configured host (for proxy setups) - don't add port + baseUrl = getLocalAppUrl(); + } else { + // Use localhost with port + baseUrl = `${getLocalAppUrl()}:${defaultPort}`; + } + if (pageUrl === undefined) { - pageUrl = serverUrl; + pageUrl = baseUrl; } else if (pageUrl.startsWith('/')) { - // construct page url for page path wihtout host and port - pageUrl = `${localAppUrl}:${defaultPort}${pageUrl}`; + // construct page url for page path without host and port + pageUrl = `${baseUrl}${pageUrl}`; } - // get external web page url - let pageUri: Uri = await env.asExternalUri(Uri.parse(pageUrl)); - - // update active server port number - if (!_running) { - //pageUri.authority.startsWith(localhost) && !isServerRunning()) { + // update active server port number (only for localhost) + if (!_running && !allowedHost) { // get the next available localhost port number _activePort = await tryPort(defaultPort); + + // rewrite requested app page url to use the new active localhost server port + pageUrl = pageUrl.replace(`:${defaultPort}/`, `:${_activePort}/`); } - // rewrite requested app page url to use the new active localhost server port - pageUri = Uri.parse( - pageUri - .toString(true) // skip encoding - .replace(`:${defaultPort}/`, `:${_activePort}/`) - ); + // get external web page url (after port has been finalized) + let pageUri: Uri = await env.asExternalUri(Uri.parse(pageUrl)); + + // TEST: let's see what asExternalUri gives us for localhost + const testLocalhostUrl = `http://localhost:${_activePort}${pageUrl.includes('/') ? pageUrl.split('/').slice(-1)[0] : ''}`; + const testPageUri: Uri = await env.asExternalUri(Uri.parse(testLocalhostUrl)); const outputChannel = getOutputChannel(); - outputChannel.appendLine(`Requested app page: ${pageUri.toString(true)}`); // skip encoding + outputChannel.appendLine(`Original pageUrl: ${pageUrl}`); + outputChannel.appendLine(`Test localhost URL: ${testLocalhostUrl}`); + outputChannel.appendLine(`Test asExternalUri result: ${testPageUri.toString(true)}`); + outputChannel.appendLine(`Actual pageUri: ${pageUri.toString(true)}`); // skip encoding return pageUri; } @@ -137,9 +149,28 @@ export async function startServer(pageUri?: Uri) { serverPortParameter = ''; } + // prepare environment variables for Evidence CLI + const allowedHost = getConfig(Settings.AllowedHost, ''); + const basePath = getConfig(Settings.BasePath, ''); + const disableAutoBuilding = getConfig(Settings.DisableAutoSourceBuilding, false); + + let envVars = ''; + if (allowedHost) { + envVars += `EVIDENCE_ALLOWED_HOST=${allowedHost} `; + } + if (basePath) { + envVars += `EVIDENCE_BASE_PATH=${basePath} `; + } + + // prepare disable flags for automatic source building + let disableFlags = ''; + if (disableAutoBuilding) { + disableFlags = ' --disable-watchers sources,queries --disable-hmr sources,queries'; + } + // start dev server via terminal command sendCommand( - `${cdCommand}${dependencyCommand}${sourcesCommand}npm exec evidence dev --${devServerHostParameter}${serverPortParameter}${previewParameter}${cdBackCommand}` + `${cdCommand}${dependencyCommand}${sourcesCommand}${envVars}npm exec evidence dev --${devServerHostParameter}${serverPortParameter}${previewParameter}${disableFlags}${cdBackCommand}` ); } @@ -171,7 +202,15 @@ export async function startServer(pageUri?: Uri) { // open app preview if previewType is set to internal (simple browser) if (previewType === 'internal' || previewType === 'internal - side-by-side') { - preview(pageUri); + const previewDelay = getConfig(Settings.PreviewDelay, 0) as number; + if (previewDelay > 0) { + // Add delay for Cloudflare tunnels or slow proxy setups + setTimeout(() => { + preview(pageUri); + }, previewDelay * 1000); + } else { + preview(pageUri); + } } // change button to stop server diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index 2255eddc20..7b59033afe 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -12,7 +12,11 @@ export const enum Settings { AutoStart = 'autoStart', TemplateProjectUrl = 'templateProjectUrl', PreviewType = 'previewType', - SlashCommands = 'slashCommands' + SlashCommands = 'slashCommands', + AllowedHost = 'allowedHost', + BasePath = 'basePath', + DisableAutoSourceBuilding = 'disableAutoSourceBuilding', + PreviewDelay = 'previewDelay' } /** diff --git a/packages/extension/src/node.ts b/packages/extension/src/node.ts index efb39fe5e9..9d7e1c82f2 100644 --- a/packages/extension/src/node.ts +++ b/packages/extension/src/node.ts @@ -7,18 +7,38 @@ const downloadNodeJs = 'Download NodeJS (LTS Version)'; const downloadNodeJsUrl = 'https://nodejs.org/en/download'; /** - * Gets NodeJS version. + * Gets NodeJS version using the user's login shell environment. + * This works with version managers like asdf, nvm, fnm, volta, etc. * * @returns The NodeJS version. */ export async function getNodeVersion() { - let nodeVersion; + // Strategy 1: Use login shell in current workspace directory + // This loads the user's shell configuration and version manager setup try { - nodeVersion = await executeCommand('node --version'); + const { workspace } = await import('vscode'); + const workspaceFolder = workspace.workspaceFolders?.[0]; + const cwd = workspaceFolder?.uri.fsPath || process.cwd(); + + const nodeVersion = await executeShellCommand('node --version', cwd); + if (nodeVersion && nodeVersion.startsWith('v')) { + return nodeVersion; + } } catch (e) { - nodeVersion = 'none'; + // Continue to fallback strategy } - return nodeVersion; + + // Strategy 2: Fallback to direct command (for system-installed Node) + try { + const nodeVersion = await executeCommand('node --version'); + if (nodeVersion && nodeVersion.trim().startsWith('v')) { + return nodeVersion.trim(); + } + } catch (e) { + // Node not found + } + + return 'none'; } /** @@ -89,6 +109,30 @@ export function executeCommand(command: string): Promise { }); } +/** + * Executes command using the user's login shell to load version manager environments. + * This works with asdf, nvm, fnm, volta, and other version managers. + * + * @param command The command to execute. + * @param cwd Optional working directory. + * @returns The stdout of the executed command. + */ +export function executeShellCommand(command: string, cwd?: string): Promise { + return new Promise((resolve, reject) => { + // Use login shell to load user's environment (version managers) + const shell = process.env.SHELL || '/bin/bash'; + const shellCommand = `${shell} -l -c "${command}"`; + + exec(shellCommand, { cwd }, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }); + }); +} + export async function promptToInstallNodeJsAndRestart(currentVersion: string | undefined) { const context = getExtensionContext(); const nodeErrorCountKey = 'nodeErrorCount'; diff --git a/packages/extension/src/terminal.ts b/packages/extension/src/terminal.ts index a074c5f634..624eda235a 100644 --- a/packages/extension/src/terminal.ts +++ b/packages/extension/src/terminal.ts @@ -96,6 +96,7 @@ export async function sendCommand( export function closeTerminal() { if (_terminal) { _terminal.show(false); + // Send Ctrl+C to stop the running process _terminal.sendText(`\x03`); _terminal.dispose(); _terminal = undefined; From 5c8f2143ddbaade3b4b3e0de586f8e7bba7850a2 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Sun, 21 Sep 2025 21:29:10 +0000 Subject: [PATCH 10/18] preview works with forwarding proxy but has issues --- packages/extension/src/commands/server.ts | 19 ++++- packages/extension/src/node.ts | 91 ++++++++++++++++++----- packages/extension/src/terminal.ts | 42 ++++------- packages/extension/src/utils/httpUtils.ts | 15 ++++ 4 files changed, 119 insertions(+), 48 deletions(-) diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index d954347994..f59fe90782 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -8,12 +8,12 @@ import { localAppUrl, preview } from './preview'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from '../node'; import { statusBar } from '../statusBar'; import { timeout } from '../utils/timer'; -import { tryPort } from '../utils/httpUtils'; +import { isHostname, tryPort } from '../utils/httpUtils'; import { hasDependencies } from './build'; import { telemetryService } from '../extension'; import { hasManifest, getTypesFromConnections, getPackageJsonFolder } from '../utils/jsonUtils'; -const localhost = 'localhost'; +const localhost = ['127.0.0.1', '::1', 'localhost']; let _running: boolean = false; let _activePort: number = getConfig(Settings.DefaultPort); @@ -78,6 +78,8 @@ export async function startServer(pageUri?: Uri) { pageUri = await getAppPageUri('/'); } + const pageHostname = pageUri.authority.split(':')[0]; + // check if we need to run command in a different directory than root of the project: const workspaceFolderPath = workspace.workspaceFolders ? workspace.workspaceFolders[0].uri.fsPath @@ -126,7 +128,7 @@ export async function startServer(pageUri?: Uri) { let serverPortParameter = ` --port ${_activePort}`; let devServerHostParameter: string = ''; - if (!pageUri.authority.startsWith(localhost)) { + if (!localhost.includes(pageHostname)) { // use remote host parameter to start dev server on github codespaces devServerHostParameter = ' --host 0.0.0.0'; } @@ -137,9 +139,18 @@ export async function startServer(pageUri?: Uri) { serverPortParameter = ''; } + let envVariables: string = ''; + if (isHostname(pageHostname)) { + envVariables += `EVIDENCE_ALLOWED_HOST=${pageHostname} `; + } + + if (!(['', '/'].includes(pageUri.path))) { + envVariables += `EVIDENCE_BASE_PATH=${pageUri.path.replace(/\/$/, '')} `; + } + // start dev server via terminal command sendCommand( - `${cdCommand}${dependencyCommand}${sourcesCommand}npm exec evidence dev --${devServerHostParameter}${serverPortParameter}${previewParameter}${cdBackCommand}` + `${cdCommand}${dependencyCommand}${sourcesCommand}${envVariables}npm exec evidence dev --${devServerHostParameter}${serverPortParameter}${previewParameter}${cdBackCommand}` ); } diff --git a/packages/extension/src/node.ts b/packages/extension/src/node.ts index efb39fe5e9..f136165dee 100644 --- a/packages/extension/src/node.ts +++ b/packages/extension/src/node.ts @@ -2,23 +2,63 @@ import { exec } from 'child_process'; import { window, env, ExtensionContext, Uri } from 'vscode'; import { showRestartPrompt } from './views/prompts'; import { getExtensionContext } from './extensionContext'; +import { getWorkspaceFolder } from './config'; -const downloadNodeJs = 'Download NodeJS (LTS Version)'; +const downloadNodeJs = 'Download NodeJS (LTS Version)'; const downloadNodeJsUrl = 'https://nodejs.org/en/download'; /** - * Gets NodeJS version. + * Gets command version while trying to use the user's login shell + * environment. This is in order to support the variety of tooling + * and version managers there are: such as asdf, nvm, fnm, etc. * - * @returns The NodeJS version. + * @param {'node' | 'node'} cmd + * @returns The tooling cmd version. */ -export async function getNodeVersion() { - let nodeVersion; +async function getToolingVersion(cmd: 'node' | 'npm') { + // Strategy 1: Use login shell in current workspace directory + // This loads the user's shell configuration and version manager setup + try { + const workspaceFolder = getWorkspaceFolder(); + const cwd = workspaceFolder?.uri.fsPath || process.cwd(); + + const cmdVersion = await executeShellCommand(`${cmd} --version`, cwd); + if (typeof cmdVersion === 'string' && cmdVersion.length > 0) { + return cmdVersion; + } + } catch (e) { + // Continue to fallback strategy + } + + // Strategy 2: Fallback to direct command (for system-installed Node) try { - nodeVersion = await executeCommand('node --version'); + const cmdVersion = await executeCommand(`${cmd} --version`); + if (typeof cmdVersion === 'string' && cmdVersion.length > 0) { + return cmdVersion; + } } catch (e) { - nodeVersion = 'none'; + // cmd not found } - return nodeVersion; + + return 'none'; +} + +/** + * Gets NodeJS version. + * + * @returns The NodeJS version (e.g. 'v22.19.0'). + */ +export async function getNodeVersion() { + return await getToolingVersion('node'); +} + +/** + * Gets NPM version. + * + * @returns The NPM version (e.g. '10.9.3'). + */ +export async function getNpmVersion() { + return await getToolingVersion('npm'); } /** @@ -60,15 +100,6 @@ export function isSupportedNodeVersion(nodeVersion: string): boolean { return false; } -/** - * Gets NPM version. - * - * @returns The NPM version. - */ -export async function getNpmVersion() { - return await executeCommand('npm --version'); -} - /** * Executes command using node child_process.exec. * @@ -83,7 +114,31 @@ export function executeCommand(command: string): Promise { if (error) { reject(error); } else { - resolve(stdout); + resolve(stdout.trim()); + } + }); + }); +} + +/** + * Executes command using the user's login shell to load version manager environments. + * This works with asdf, nvm, fnm, volta, and other version managers. + * + * @param command The command to execute. + * @param cwd Optional working directory. + * @returns The stdout of the executed command. + */ +export function executeShellCommand(command: string, cwd?: string): Promise { + return new Promise((resolve, reject) => { + // Use login shell to load user's environment (version managers) + const shell = process.env.SHELL || '/bin/bash'; + const shellCommand = `${shell} -l -c "${command}"`; + + exec(shellCommand, { cwd }, (error, stdout, _stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); } }); }); diff --git a/packages/extension/src/terminal.ts b/packages/extension/src/terminal.ts index a074c5f634..05178a7b10 100644 --- a/packages/extension/src/terminal.ts +++ b/packages/extension/src/terminal.ts @@ -1,4 +1,4 @@ -import { env, window, Disposable, ExtensionContext, OutputChannel, Terminal, Uri } from 'vscode'; +import { window, ExtensionContext, OutputChannel, Terminal } from 'vscode'; import { getExtensionContext } from './extensionContext'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from './node'; @@ -12,11 +12,11 @@ const terminalName = 'Evidence'; /** * Evidence terminal instance. */ -let _terminal: Terminal | undefined; +let _terminal: () => Terminal | undefined = () => + window.terminals.find(terminal => terminal.name === terminalName); let _outputChannel: OutputChannel | undefined; let _nodeVersion: string | undefined; let _currentDirectory: string | undefined; -let _disposable: Disposable | undefined; /** * Gets Evidence treminal instance. @@ -26,36 +26,26 @@ let _disposable: Disposable | undefined; * @returns VScode Terminal instance. */ async function getTerminal( - context: ExtensionContext, + _context: ExtensionContext, workingDirectory?: string ): Promise { _outputChannel = getOutputChannel(); - if (_terminal === undefined) { - _terminal = window.createTerminal(terminalName); - _terminal.show(false); - // _terminal.sendText('node -v'); + + let terminal = _terminal(); + if (terminal === undefined) { + terminal = window.createTerminal(terminalName); + terminal.show(false); _nodeVersion = await getNodeVersion(); _outputChannel.appendLine(`Using node ${_nodeVersion}`); - - // dispose this terminal when terminal panel is closed - _disposable = window.onDidCloseTerminal((e: Terminal) => { - if (e.name === terminalName) { - _terminal = undefined; - _disposable?.dispose(); - _disposable = undefined; - } - }); - - context.subscriptions.push(_disposable); _currentDirectory = undefined; } if (_currentDirectory !== workingDirectory && workingDirectory && workingDirectory.length > 0) { - _terminal.sendText(`cd "${workingDirectory}"`, true); // add new line + terminal.sendText(`cd "${workingDirectory}"`, true); // add new line _currentDirectory = workingDirectory; } - return _terminal; + return terminal; } /** @@ -94,10 +84,10 @@ export async function sendCommand( * Closes active Evidence app terminal. */ export function closeTerminal() { - if (_terminal) { - _terminal.show(false); - _terminal.sendText(`\x03`); - _terminal.dispose(); - _terminal = undefined; + const terminal = _terminal(); + if (terminal !== undefined) { + terminal.show(false); + terminal.sendText(`\x03`); + terminal.dispose(); } } diff --git a/packages/extension/src/utils/httpUtils.ts b/packages/extension/src/utils/httpUtils.ts index 10952cd4c4..55beb165b1 100644 --- a/packages/extension/src/utils/httpUtils.ts +++ b/packages/extension/src/utils/httpUtils.ts @@ -94,3 +94,18 @@ export async function waitFor(url: string, interval = 200, max = 30_000) { return false; } + +/** + * Checks if the authority part of a URL is a host name + * rather than an IP address. + * + * @param hostname The host part of a URL. + * + * @returns True if it's not an IP address, false if it is + */ +export function isHostname(hostname: string) { + const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; + const ipv6Pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + return !ipv4Pattern.test(hostname) && + !ipv6Pattern.test(hostname); +} \ No newline at end of file From c2ce90f3ef677418aec70e65d33a6d0dfb33440f Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 10:17:07 +0000 Subject: [PATCH 11/18] a robust _running/isServerRunning() implementation --- packages/extension/src/commands/build.ts | 8 +- packages/extension/src/commands/preview.ts | 8 +- packages/extension/src/commands/server.ts | 32 +++++--- packages/extension/src/commands/settings.ts | 2 +- packages/extension/src/commands/sources.ts | 2 +- packages/extension/src/node.ts | 47 +----------- packages/extension/src/terminal.ts | 4 + packages/extension/src/utils/shellUtils.ts | 84 +++++++++++++++++++++ 8 files changed, 120 insertions(+), 67 deletions(-) create mode 100644 packages/extension/src/utils/shellUtils.ts diff --git a/packages/extension/src/commands/build.ts b/packages/extension/src/commands/build.ts index 495ad79380..3b22ad32fb 100644 --- a/packages/extension/src/commands/build.ts +++ b/packages/extension/src/commands/build.ts @@ -44,7 +44,7 @@ export async function installDependencies() { if (!isSupportedNodeVersion(nodeVersion)) { promptToInstallNodeJsAndRestart(nodeVersion); } else { - if (isServerRunning()) { + if (await isServerRunning()) { stopServer(); await timeout(1000); } else { @@ -72,7 +72,7 @@ export async function updateDependencies() { const cdCommand = packageJsonFolder ? `cd ${packageJsonFolder} ; ` : ''; const cdBackCommand = packageJsonFolder ? `; cd ${workspaceFolderPath}` : ''; - if (isServerRunning()) { + if (await isServerRunning()) { stopServer(); await timeout(1000); } @@ -89,7 +89,7 @@ export async function updateDependencies() { * @see https://docs.evidence.dev/deployment/overview#build-process */ export async function buildProject() { - if (isServerRunning()) { + if (await isServerRunning()) { stopServer(); await timeout(1000); } @@ -103,7 +103,7 @@ export async function buildProject() { * @see https://docs.evidence.dev/deployment/overview#buildstrict */ export async function buildProjectStrict() { - if (isServerRunning()) { + if (await isServerRunning()) { stopServer(); await timeout(1000); } diff --git a/packages/extension/src/commands/preview.ts b/packages/extension/src/commands/preview.ts index d620175e50..96445e9b87 100644 --- a/packages/extension/src/commands/preview.ts +++ b/packages/extension/src/commands/preview.ts @@ -25,7 +25,7 @@ export const localAppUrl = `http://localhost`; * For the Evidence markdown documents in the /pages/ folder, * opens the requested app page in the built-in simple browser webview. * - * @param uri Optional Uri of the markdown document to preview. + * @param uri Optional local Uri of the markdown document to preview. * * @see Simple browser extension implementation: * https://github.com/microsoft/vscode/pull/109276 @@ -37,7 +37,7 @@ export async function preview(uri?: Uri) { // check if the open workspace has an Evidence project const isEvidenceProject = getExtensionContext().workspaceState.get(Context.HasEvidenceProject); - if ((!uri || uri.path === '/') && isEvidenceProject && isServerRunning()) { + if ((!uri || uri.path === '/') && isEvidenceProject && (await isServerRunning())) { // open the default app page in the built-in simple browser webview const homePage: Uri = await getAppPageUri('/'); openPageView(homePage); @@ -62,7 +62,7 @@ export async function preview(uri?: Uri) { } } - if (!isEvidenceProject || !isServerRunning() || /\/pages\/|\\pages\\/.test(uri?.path ?? '')) { + if (!isEvidenceProject || !(await isServerRunning()) || /\/pages\/|\\pages\\/.test(uri?.path ?? '')) { // show standard markdown document preview commands.executeCommand(Commands.MarkdownShowPreview, uri); return; @@ -76,7 +76,7 @@ export async function preview(uri?: Uri) { uri.scheme === 'file' && workspace.workspaceFolders && isEvidenceProject && - isServerRunning() + (await isServerRunning()) ) { // get project folder root path const workspaceFolderPath: string = getWorkspaceFolder()!.uri.fsPath; diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index dfffb10c1e..edd0ec69ea 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -1,7 +1,7 @@ import { commands, env, workspace, Uri } from 'vscode'; import { Commands } from './commands'; -import { Settings, getConfig } from '../config'; +import { Settings, getConfig, getWorkspaceFolder } from '../config'; import { getOutputChannel } from '../output'; import { closeTerminal, sendCommand } from '../terminal'; import { localAppUrl, preview } from './preview'; @@ -12,9 +12,19 @@ import { isHostname, tryPort } from '../utils/httpUtils'; import { hasDependencies } from './build'; import { telemetryService } from '../extension'; import { hasManifest, getTypesFromConnections, getPackageJsonFolder } from '../utils/jsonUtils'; +import { isProcessRunning } from '../utils/shellUtils'; const localhost = ['127.0.0.1', '::1', 'localhost']; -let _running: boolean = false; +// Formerly this was managed as a simple boolean variable which was problematic +// in a workspace extension in remote development scenarios. +// (namely, we often ended up with the _running variable being false +// while the Evidence dev server launched by the extension is still up and running) +const _running = async (): Promise => { + const workspaceFolder = getWorkspaceFolder(); + if (!workspaceFolder) return false; + + return await isProcessRunning('evidence dev', workspaceFolder.uri.fsPath); +}; let _activePort: number = getConfig(Settings.DefaultPort); // Set a context key @@ -47,7 +57,7 @@ export async function getAppPageUri(pageUrl?: string): Promise { let pageUri: Uri = await env.asExternalUri(Uri.parse(pageUrl)); // update active server port number - if (!_running) { + if (!(await _running())) { //pageUri.authority.startsWith(localhost) && !isServerRunning()) { // get the next available localhost port number _activePort = await tryPort(defaultPort); @@ -69,7 +79,7 @@ export async function getAppPageUri(pageUrl?: string): Promise { * Starts Evidence app dev server, and opens Evidence app preview * in the built-in vscode simple browser. * - * @param pageFileUri Optional Uri of the starting page to load in preview. + * @param pageFileUri Optional local Uri of the starting page to load in preview. */ export async function startServer(pageUri?: Uri) { telemetryService?.sendEvent('startServer'); @@ -123,7 +133,7 @@ export async function startServer(pageUri?: Uri) { telemetryService?.sendEvent('runSources', { sources: sourceNames.join(', ') }); } - if (!_running) { + if (!(await _running())) { // use the last saved active port number to start dev server if using simple browser let serverPortParameter = ` --port ${_activePort}`; @@ -166,8 +176,7 @@ export async function startServer(pageUri?: Uri) { // update server status and show running status bar icon statusBar.showRunning(); - _running = true; - setContext('evidence.serverRunning', _running); + setContext('evidence.serverRunning', true); if (previewType.includes('internal')) { // wait for the dev server to start @@ -182,7 +191,7 @@ export async function startServer(pageUri?: Uri) { } } - if (_running === true) { + if (await _running()) { // set focus back to the active vscode editor group commands.executeCommand(Commands.FocusActiveEditorGroup); @@ -202,8 +211,8 @@ export async function startServer(pageUri?: Uri) { * * @returns True if Evidence dev server is running, and false otherwise. */ -export function isServerRunning() { - return _running; +export async function isServerRunning() { + return await _running(); } /** @@ -229,8 +238,7 @@ export async function stopServer() { closeTerminal(); // reset server state and status display - _running = false; - setContext('evidence.serverRunning', _running); + setContext('evidence.serverRunning', false); _activePort = getConfig(Settings.DefaultPort); statusBar.showStart(); telemetryService?.sendEvent('stopServer'); diff --git a/packages/extension/src/commands/settings.ts b/packages/extension/src/commands/settings.ts index 3feb9103c8..292ffdb2dd 100644 --- a/packages/extension/src/commands/settings.ts +++ b/packages/extension/src/commands/settings.ts @@ -30,7 +30,7 @@ export async function viewExtensionSettings() { */ export async function viewAppSettings() { const settingsPageUri: Uri = await getAppPageUri(settingsPagePath); - if (!isServerRunning()) { + if (!(await isServerRunning())) { startServer(settingsPageUri); } else { // show app settings page in simple browser webview diff --git a/packages/extension/src/commands/sources.ts b/packages/extension/src/commands/sources.ts index 265bf7631f..bf8a644751 100644 --- a/packages/extension/src/commands/sources.ts +++ b/packages/extension/src/commands/sources.ts @@ -7,7 +7,7 @@ import { workspace } from 'vscode'; export async function runSources() { let serverRunning = false; - if (isServerRunning()) { + if (await isServerRunning()) { serverRunning = true; stopServer(); await timeout(1000); diff --git a/packages/extension/src/node.ts b/packages/extension/src/node.ts index f136165dee..c94e58c609 100644 --- a/packages/extension/src/node.ts +++ b/packages/extension/src/node.ts @@ -1,8 +1,8 @@ -import { exec } from 'child_process'; -import { window, env, ExtensionContext, Uri } from 'vscode'; +import { window, env, Uri } from 'vscode'; import { showRestartPrompt } from './views/prompts'; import { getExtensionContext } from './extensionContext'; import { getWorkspaceFolder } from './config'; +import { executeCommand, executeShellCommand } from './utils/shellUtils'; const downloadNodeJs = 'Download NodeJS (LTS Version)'; const downloadNodeJsUrl = 'https://nodejs.org/en/download'; @@ -100,49 +100,6 @@ export function isSupportedNodeVersion(nodeVersion: string): boolean { return false; } -/** - * Executes command using node child_process.exec. - * - * @see https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback - * - * @param command The node command to execute. - * @returns The stdout of the executed command. - */ -export function executeCommand(command: string): Promise { - return new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - reject(error); - } else { - resolve(stdout.trim()); - } - }); - }); -} - -/** - * Executes command using the user's login shell to load version manager environments. - * This works with asdf, nvm, fnm, volta, and other version managers. - * - * @param command The command to execute. - * @param cwd Optional working directory. - * @returns The stdout of the executed command. - */ -export function executeShellCommand(command: string, cwd?: string): Promise { - return new Promise((resolve, reject) => { - // Use login shell to load user's environment (version managers) - const shell = process.env.SHELL || '/bin/bash'; - const shellCommand = `${shell} -l -c "${command}"`; - - exec(shellCommand, { cwd }, (error, stdout, _stderr) => { - if (error) { - reject(error); - } else { - resolve(stdout.trim()); - } - }); - }); -} export async function promptToInstallNodeJsAndRestart(currentVersion: string | undefined) { const context = getExtensionContext(); diff --git a/packages/extension/src/terminal.ts b/packages/extension/src/terminal.ts index 05178a7b10..3594cdf6f5 100644 --- a/packages/extension/src/terminal.ts +++ b/packages/extension/src/terminal.ts @@ -12,6 +12,10 @@ const terminalName = 'Evidence'; /** * Evidence terminal instance. */ +// Formerly this was managed as a simple variable which was problematic +// in a workspace extension in remote development scenarios. +// (namely, we often ended up with the _terminal variable being undefined +// while the Terminal launched by the extension is still up and running) let _terminal: () => Terminal | undefined = () => window.terminals.find(terminal => terminal.name === terminalName); let _outputChannel: OutputChannel | undefined; diff --git a/packages/extension/src/utils/shellUtils.ts b/packages/extension/src/utils/shellUtils.ts new file mode 100644 index 0000000000..420bbd00a9 --- /dev/null +++ b/packages/extension/src/utils/shellUtils.ts @@ -0,0 +1,84 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +/** + * Checks if a process with given command is running in the specified directory + */ +export async function isProcessRunning( + command: string, + workingDirectory: string +): Promise { + try { + // On Unix-like systems (including WSL) + const { stdout } = await execAsync( + `ps aux | grep "${command}" | grep -v grep | grep "${workingDirectory}"` + ); + return stdout.trim().length > 0; + } catch { + return false; + } +} + +/** + * Gets process ID for a command running in specified directory + */ +export async function getProcessId( + command: string, + workingDirectory: string +): Promise { + try { + const { stdout } = await execAsync( + `ps aux | grep "${command}" | grep -v grep | grep "${workingDirectory}" | awk '{print $2}'` + ); + const pid = parseInt(stdout.trim().split('\n')[0]); + return isNaN(pid) ? undefined : pid; + } catch { + return undefined; + } +} + +/** + * Executes command using node child_process.exec. + * + * @see https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback + * + * @param command The node command to execute. + * @returns The stdout of the executed command. + */ +export function executeCommand(command: string): Promise { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }); + }); +} + +/** + * Executes command using the user's login shell to load version manager environments. + * This works with asdf, nvm, fnm, volta, and other version managers. + * + * @param command The command to execute. + * @param cwd Optional working directory. + * @returns The stdout of the executed command. + */ +export function executeShellCommand(command: string, cwd?: string): Promise { + return new Promise((resolve, reject) => { + // Use login shell to load user's environment (version managers) + const shell = process.env.SHELL || '/bin/bash'; + const shellCommand = `${shell} -l -c "${command}"`; + + exec(shellCommand, { cwd }, (error, stdout, _stderr) => { + if (error) { + reject(error); + } else { + resolve(stdout.trim()); + } + }); + }); +} \ No newline at end of file From b9172a83132b67f64226aa6de768bcd6b8604d47 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 17:00:31 +0000 Subject: [PATCH 12/18] add missing change for disable building sources --- packages/extension/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/extension/package.json b/packages/extension/package.json index 17fe3b4d41..a6b0c9ee58 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -328,6 +328,11 @@ "default": 3000, "description": "Default Evidence server port." }, + "evidence.disableAutoSourceBuilding": { + "type": "boolean", + "default": false, + "description": "Disable the automatic building of sources by the server. When checked, the server will not automatically rebuild sources or reload when source files change." + }, "evidence.templateProjectUrl": { "type": "string", "default": "https://github.com/evidence-dev/template", From c639d492af913c74e8a08df83b69b0df56ab847b Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 19:14:45 +0000 Subject: [PATCH 13/18] preview now only receives local file paths Fixes dual-use preview function by directly opening Evidence web app by the startServer function instead of routing through markdown preview logic. (which usually receives local file paths not web app URLs) --- packages/extension/src/commands/server.ts | 14 ++++++++++---- packages/extension/src/commands/settings.ts | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index edd0ec69ea..0b44459aed 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -1,10 +1,10 @@ -import { commands, env, workspace, Uri } from 'vscode'; +import { commands, env, workspace, Uri, ViewColumn } from 'vscode'; import { Commands } from './commands'; import { Settings, getConfig, getWorkspaceFolder } from '../config'; import { getOutputChannel } from '../output'; import { closeTerminal, sendCommand } from '../terminal'; -import { localAppUrl, preview } from './preview'; +import { localAppUrl } from './preview'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from '../node'; import { statusBar } from '../statusBar'; import { timeout } from '../utils/timer'; @@ -79,7 +79,8 @@ export async function getAppPageUri(pageUrl?: string): Promise { * Starts Evidence app dev server, and opens Evidence app preview * in the built-in vscode simple browser. * - * @param pageFileUri Optional local Uri of the starting page to load in preview. + * @param pageFileUri Optional (internal) Uri of the starting page + * to be loaded in the preview. */ export async function startServer(pageUri?: Uri) { telemetryService?.sendEvent('startServer'); @@ -197,7 +198,12 @@ export async function startServer(pageUri?: Uri) { // open app preview if previewType is set to internal (simple browser) if (previewType === 'internal' || previewType === 'internal - side-by-side') { - preview(pageUri); + // directly open the web URL in simple browser instead of using preview() + commands.executeCommand(Commands.OpenSimpleBrowser, pageUri.toString(true), { + viewColumn: previewType === 'internal' ? ViewColumn.Active : ViewColumn.Two, + preserveFocus: true + }); + telemetryService?.sendEvent('openSimpleBrowser'); } // change button to stop server diff --git a/packages/extension/src/commands/settings.ts b/packages/extension/src/commands/settings.ts index 292ffdb2dd..79cee25473 100644 --- a/packages/extension/src/commands/settings.ts +++ b/packages/extension/src/commands/settings.ts @@ -27,6 +27,8 @@ export async function viewExtensionSettings() { /** * Opens Evidence app settings page in the built-in vscode simple browser webview. + * + * @deprecated */ export async function viewAppSettings() { const settingsPageUri: Uri = await getAppPageUri(settingsPagePath); From a3f197da1ce4b7aa9c55d19d1c904710df28e94d Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 19:45:31 +0000 Subject: [PATCH 14/18] fix port handling in internal urls in getAppPageUri --- packages/extension/src/commands/server.ts | 50 ++++++++++++----------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index 0b44459aed..3401004766 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -1,4 +1,4 @@ -import { commands, env, workspace, Uri, ViewColumn } from 'vscode'; +import { window, commands, env, workspace, Uri, ViewColumn } from 'vscode'; import { Commands } from './commands'; import { Settings, getConfig, getWorkspaceFolder } from '../config'; @@ -37,24 +37,15 @@ const setContext = (key: any, value: any) => { * and rewrites the host name for the host and port forwarding * when running in GitHub Codespaces. * - * @param pageUrl Optional target web page Url. + * @param pageUrl Optional internal app Url or only the path + * part of the Url (e.g. /customers). * - * @returns Rewritten page Uri with the active server port number, - * and rewritten host name for the host and port forwarding - * when running in GitHub Codespaces. + * @returns External Url which can be safely opened in a browser + * on the local system even when vscode is running remotely (e.g. + * in code-server, using SSH, or in GitHub Codespaces) */ -export async function getAppPageUri(pageUrl?: string): Promise { +export async function getAppPageUri(internalPageUrl?: string): Promise { const defaultPort = getConfig(Settings.DefaultPort); - const serverUrl = `${localAppUrl}:${defaultPort}`; - if (pageUrl === undefined) { - pageUrl = serverUrl; - } else if (pageUrl.startsWith('/')) { - // construct page url for page path wihtout host and port - pageUrl = `${localAppUrl}:${defaultPort}${pageUrl}`; - } - - // get external web page url - let pageUri: Uri = await env.asExternalUri(Uri.parse(pageUrl)); // update active server port number if (!(await _running())) { @@ -63,16 +54,21 @@ export async function getAppPageUri(pageUrl?: string): Promise { _activePort = await tryPort(defaultPort); } - // rewrite requested app page url to use the new active localhost server port - pageUri = Uri.parse( - pageUri - .toString(true) // skip encoding - .replace(`:${defaultPort}/`, `:${_activePort}/`) - ); + const internalServerUrl = `${localAppUrl}:${_activePort}`; + if (internalPageUrl === undefined) { + internalPageUrl = internalServerUrl; + } else if (internalPageUrl.startsWith('/')) { + // construct page url for page path wihtout host and port + internalPageUrl = `${internalServerUrl}${internalPageUrl}`; + } + // get external web page url + const externalPageUri: Uri = await env.asExternalUri(Uri.parse(internalPageUrl)); + const outputChannel = getOutputChannel(); - outputChannel.appendLine(`Requested app page: ${pageUri.toString(true)}`); // skip encoding - return pageUri; + outputChannel.appendLine(`Requested app page: ${externalPageUri.toString(true)}`); // skip encoding + + return externalPageUri; } /** @@ -83,6 +79,12 @@ export async function getAppPageUri(pageUrl?: string): Promise { * to be loaded in the preview. */ export async function startServer(pageUri?: Uri) { + + if (await isServerRunning()) { + window.showInformationMessage('There is an Evidence server already running!'); + return ; + } + telemetryService?.sendEvent('startServer'); if (!pageUri) { From 2c81ce8f1fb8bf02cec5dec0a8be2a734c18a0aa Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 23:41:03 +0000 Subject: [PATCH 15/18] fixed issues in preview & generating external urls various issues related to how external urls are generated were corrected, and the preview implentation simplified. a few things became more robust though much work is needed to make the extension truly robust, especially in remote environments such as code-server --- packages/extension/package.json | 2 +- packages/extension/src/commands/preview.ts | 78 +++++++++++----------- packages/extension/src/commands/server.ts | 46 ++++++------- packages/extension/src/config.ts | 3 +- packages/extension/src/extension.ts | 8 ++- 5 files changed, 71 insertions(+), 66 deletions(-) diff --git a/packages/extension/package.json b/packages/extension/package.json index a6b0c9ee58..c0fda6a0c5 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -500,7 +500,7 @@ "completionEvents": [ "onCommand:startServer" ], - "when": "evidence.hasProject == true" + "when": "evidence.hasProject == true && !evidence.serverRunning" } ] } diff --git a/packages/extension/src/commands/preview.ts b/packages/extension/src/commands/preview.ts index 96445e9b87..d9ebfd40ea 100644 --- a/packages/extension/src/commands/preview.ts +++ b/packages/extension/src/commands/preview.ts @@ -1,4 +1,4 @@ -import { commands, workspace, Uri, ViewColumn } from 'vscode'; +import { commands, workspace, Uri, ViewColumn, window } from 'vscode'; import { Commands } from './commands'; import { getExtensionContext } from '../extensionContext'; @@ -25,22 +25,31 @@ export const localAppUrl = `http://localhost`; * For the Evidence markdown documents in the /pages/ folder, * opens the requested app page in the built-in simple browser webview. * - * @param uri Optional local Uri of the markdown document to preview. + * @param uri Optional file location of the markdown document to preview. + * No http/https Urls supported. * * @see Simple browser extension implementation: * https://github.com/microsoft/vscode/pull/109276 */ export async function preview(uri?: Uri) { - // default page url - let pageUrl: string = '/'; - // check if the open workspace has an Evidence project const isEvidenceProject = getExtensionContext().workspaceState.get(Context.HasEvidenceProject); - if ((!uri || uri.path === '/') && isEvidenceProject && (await isServerRunning())) { + // check if the Evidence dev server is running + const isEvidenceServerRunning = await isServerRunning(); + + if (!isEvidenceProject || !isEvidenceServerRunning) { + // show standard markdown document preview + commands.executeCommand(Commands.MarkdownShowPreview, uri); + return; + } + + // from this point on, we know this is an Evidence project with a server running + if (!uri || uri.path === '/') { // open the default app page in the built-in simple browser webview const homePage: Uri = await getAppPageUri('/'); - openPageView(homePage); + // I suspect this to be causing the confusing message "please refresh preview" + // openPageView(homePage); // wait for the server to load the app home page await waitFor(homePage.toString(true), 1000, 30000); // encoding, ms interval, max total wait time ms @@ -51,52 +60,43 @@ export async function preview(uri?: Uri) { return; } - if (uri) { - // log preview document or page request in output channel for troubleshooting - const outputChannel = getOutputChannel(); - if (uri.scheme === 'file ') { - outputChannel.appendLine(`Requested document preview: ${uri.fsPath}`); // skip encoding - } else if (uri.scheme === 'http' || uri.scheme === 'https') { - // must be a valide http or https - outputChannel.appendLine(`Requested page preview: ${uri.path}`); // skip encoding - } - } - - if (!isEvidenceProject || !(await isServerRunning()) || /\/pages\/|\\pages\\/.test(uri?.path ?? '')) { - // show standard markdown document preview - commands.executeCommand(Commands.MarkdownShowPreview, uri); + // from this point on, we know that "uri" is not 'undefined' or '/' + const outputChannel = getOutputChannel(); + if (uri.scheme !== 'file') { + // this should never happen + outputChannel.appendLine(`Only document preview is supported, preview received: ${ + uri.toString(true)}`); // skip encoding return; + } else { + outputChannel.appendLine(`Requested document preview: ${ + uri?.fsPath}`); // skip encoding } - // create web page url from page Uri - if (uri && (uri.scheme === 'http' || uri.scheme === 'https')) { - pageUrl = uri.toString(true); // skip encoding - } else if ( - uri && - uri.scheme === 'file' && - workspace.workspaceFolders && - isEvidenceProject && - (await isServerRunning()) - ) { + // from this point on, we know it is file uri which we need to map to an external app url + if (workspace.workspaceFolders) { + let pageUrlPath = uri.fsPath; + // get project folder root path const workspaceFolderPath: string = getWorkspaceFolder()!.uri.fsPath; // create web page url from file Uri by converting .md path to app page path - let pagePath: string = uri.fsPath + pageUrlPath = pageUrlPath .replace(workspaceFolderPath, '') .split('\\') .join('/') .replace('/pages/', '') .replace('index.md', '') .replace('.md', ''); - pageUrl = `/${pagePath}`; + pageUrlPath = `/${pageUrlPath}`; + + // create external app page Uri from page url path + const pageUri: Uri = await getAppPageUri(pageUrlPath); + + // open requested page in the built-in simple browser webview + openPageView(pageUri); } - // create external app page Uri from page url - const pageUri: Uri = await getAppPageUri(pageUrl); - - // open requested page in the built-in simple browser webview - openPageView(pageUri); + window.showErrorMessage('Preview is not possible without a folder opened.'); } /** @@ -104,7 +104,7 @@ export async function preview(uri?: Uri) { * * @param pageUri Uri of the page to open. */ -async function openPageView(pageUri: Uri) { +export async function openPageView(pageUri: Uri) { const previewType: string = getConfig(Settings.PreviewType); if (pageUri) { diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index 3401004766..875d2000fc 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -1,10 +1,10 @@ import { window, commands, env, workspace, Uri, ViewColumn } from 'vscode'; import { Commands } from './commands'; -import { Settings, getConfig, getWorkspaceFolder } from '../config'; +import { Context as ExtensionContext, Settings, getConfig, getWorkspaceFolder } from '../config'; import { getOutputChannel } from '../output'; import { closeTerminal, sendCommand } from '../terminal'; -import { localAppUrl } from './preview'; +import { localAppUrl, openPageView } from './preview'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from '../node'; import { statusBar } from '../statusBar'; import { timeout } from '../utils/timer'; @@ -13,6 +13,7 @@ import { hasDependencies } from './build'; import { telemetryService } from '../extension'; import { hasManifest, getTypesFromConnections, getPackageJsonFolder } from '../utils/jsonUtils'; import { isProcessRunning } from '../utils/shellUtils'; +import { Context } from 'mocha'; const localhost = ['127.0.0.1', '::1', 'localhost']; // Formerly this was managed as a simple boolean variable which was problematic @@ -37,14 +38,14 @@ const setContext = (key: any, value: any) => { * and rewrites the host name for the host and port forwarding * when running in GitHub Codespaces. * - * @param pageUrl Optional internal app Url or only the path - * part of the Url (e.g. /customers). + * @param pageUrlPath Optional, the path part of the app Url + * to be converted to an external app Url * * @returns External Url which can be safely opened in a browser * on the local system even when vscode is running remotely (e.g. * in code-server, using SSH, or in GitHub Codespaces) */ -export async function getAppPageUri(internalPageUrl?: string): Promise { +export async function getAppPageUri(pageUrlPath?: string): Promise { const defaultPort = getConfig(Settings.DefaultPort); // update active server port number @@ -54,16 +55,21 @@ export async function getAppPageUri(internalPageUrl?: string): Promise { _activePort = await tryPort(defaultPort); } - const internalServerUrl = `${localAppUrl}:${_activePort}`; - if (internalPageUrl === undefined) { - internalPageUrl = internalServerUrl; - } else if (internalPageUrl.startsWith('/')) { - // construct page url for page path wihtout host and port - internalPageUrl = `${internalServerUrl}${internalPageUrl}`; - } + const internalServerUrl = `${localAppUrl}:${_activePort ?? defaultPort}`; // get external web page url - const externalPageUri: Uri = await env.asExternalUri(Uri.parse(internalPageUrl)); + let externalPageUri: Uri = await env.asExternalUri(Uri.parse(internalServerUrl)); + + // it seems that "asExternalUri" only converts the host part of the Url to its + // external form, and discards the path part of the Url when present. Hence this + // should be added after the convertion to an external Url. + if (pageUrlPath?.startsWith('/')) { + // append page path to the external base path (e.g. /proxy/3710 + /another = /proxy/3710/another) + // handle potential trailing/leading slashes to avoid double slashes + const basePath = externalPageUri.path.endsWith('/') ? + externalPageUri.path.slice(0, -1) : externalPageUri.path; + externalPageUri = externalPageUri.with({ path: basePath + pageUrlPath }); + } const outputChannel = getOutputChannel(); outputChannel.appendLine(`Requested app page: ${externalPageUri.toString(true)}`); // skip encoding @@ -179,7 +185,7 @@ export async function startServer(pageUri?: Uri) { // update server status and show running status bar icon statusBar.showRunning(); - setContext('evidence.serverRunning', true); + setContext(ExtensionContext.IsServerRunning, true); if (previewType.includes('internal')) { // wait for the dev server to start @@ -199,14 +205,7 @@ export async function startServer(pageUri?: Uri) { commands.executeCommand(Commands.FocusActiveEditorGroup); // open app preview if previewType is set to internal (simple browser) - if (previewType === 'internal' || previewType === 'internal - side-by-side') { - // directly open the web URL in simple browser instead of using preview() - commands.executeCommand(Commands.OpenSimpleBrowser, pageUri.toString(true), { - viewColumn: previewType === 'internal' ? ViewColumn.Active : ViewColumn.Two, - preserveFocus: true - }); - telemetryService?.sendEvent('openSimpleBrowser'); - } + openPageView(pageUri); // change button to stop server statusBar.showStop(); @@ -246,8 +245,9 @@ export async function stopServer() { closeTerminal(); // reset server state and status display - setContext('evidence.serverRunning', false); + setContext(ExtensionContext.IsServerRunning, false); _activePort = getConfig(Settings.DefaultPort); statusBar.showStart(); telemetryService?.sendEvent('stopServer'); } + diff --git a/packages/extension/src/config.ts b/packages/extension/src/config.ts index 87dadec930..24ec493072 100644 --- a/packages/extension/src/config.ts +++ b/packages/extension/src/config.ts @@ -20,7 +20,8 @@ export const enum Settings { * Evidence extension context and state keys. */ export const enum Context { - HasEvidenceProject = 'evidence.hasProject' + HasEvidenceProject = 'evidence.hasProject', + IsServerRunning = 'evidence.serverRunning', } /** diff --git a/packages/extension/src/extension.ts b/packages/extension/src/extension.ts index d979b56ce5..7d174c7f88 100644 --- a/packages/extension/src/extension.ts +++ b/packages/extension/src/extension.ts @@ -37,7 +37,7 @@ import { hasManifest } from './utils/jsonUtils'; import { Settings, getConfig, updateProjectContext } from './config'; -import { startServer } from './commands/server'; +import { isServerRunning, startServer } from './commands/server'; import { openIndex } from './commands/project'; import { statusBar } from './statusBar'; import { closeTerminal } from './terminal'; @@ -1643,7 +1643,11 @@ export async function activate(context: ExtensionContext) { const autoStart: boolean = getConfig(Settings.AutoStart); // show start dev server status - statusBar.showStart(); + if (!(await isServerRunning())) { + statusBar.showStart(); + } else { + statusBar.showStop(); + } // open index.md if no other files are open openIndex(); From 01c837d964bbb78340ace7813ed53bea18f56232 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Mon, 22 Sep 2025 23:44:10 +0000 Subject: [PATCH 16/18] front-end routing now supports EVIDENCE_BASE_PATH --- .../src/lib/organisms/layout/BreadCrumbs.svelte | 6 ++++-- .../organisms/layout/EvidenceDefaultLayout.svelte | 12 ++++++------ .../src/lib/organisms/layout/sidebar/Sidebar.svelte | 13 +++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte b/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte index 41263f2b0a..e951e7dc37 100644 --- a/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte +++ b/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte @@ -19,7 +19,7 @@ export function buildCrumbs(pathArray, fileTree) { const crumbs = [{ href: '/', title: 'Home' }]; pathArray.forEach((path, i) => { - if (path != '' && `/${path}` !== config.deployment.basePath) { + if (path != '') { crumbs.push({ href: '/' + pathArray.slice(0, i + 1).join('/'), title: decodeURIComponent(path.replace(/_/g, ' ').replace(/-/g, ' ')) @@ -57,7 +57,9 @@ import { addBasePath } from '@evidence-dev/sdk/utils/svelte'; export let fileTree; - $: crumbs = buildCrumbs($page.url.pathname.split('/').slice(1), fileTree); + let crumbs = []; + + $: crumbs = buildCrumbs($page.url.pathname.replace(config.deployment.basePath, '').split('/').slice(1), fileTree);
diff --git a/packages/ui/core-components/src/lib/organisms/layout/EvidenceDefaultLayout.svelte b/packages/ui/core-components/src/lib/organisms/layout/EvidenceDefaultLayout.svelte index 4c27e4652e..1c3dd68581 100644 --- a/packages/ui/core-components/src/lib/organisms/layout/EvidenceDefaultLayout.svelte +++ b/packages/ui/core-components/src/lib/organisms/layout/EvidenceDefaultLayout.svelte @@ -99,7 +99,7 @@ $: fileMap = convertFileTreeToFileMap(fileTree); - $: routeFrontMatter = fileMap.get($page.route.id)?.frontMatter; + $: routeFrontMatter = fileMap.get($page.route?.id)?.frontMatter; $: sidebarFrontMatter = routeFrontMatter?.sidebar; @@ -213,7 +213,7 @@ 'print:w-[650px] print:md:w-[841px] mx-auto print:md:px-0 print:px-0 px-6 sm:px-8 md:px-12 flex justify-start'} style="max-width:{maxWidthEffective}px;" > - {#if !hideSidebar && sidebarFrontMatter !== 'never' && $page.route.id !== '/settings'} + {#if !hideSidebar && sidebarFrontMatter !== 'never' && $page.route?.id !== '/settings'}
{/if}
- {#if !hideBreadcrumbsEffective && $page.route.id !== '/settings'} + {#if !hideBreadcrumbsEffective && $page.route?.id !== '/settings'}
- {#if $page.route.id !== '/settings'} + {#if $page.route?.id !== '/settings'} {/if}
@@ -256,7 +256,7 @@ {/if}
- {#if !hideTocEffective && $page.route.id !== '/settings'} + {#if !hideTocEffective && $page.route?.id !== '/settings'}
diff --git a/packages/ui/core-components/src/lib/organisms/layout/sidebar/Sidebar.svelte b/packages/ui/core-components/src/lib/organisms/layout/sidebar/Sidebar.svelte index 53ec74c0bb..9993e8d75c 100644 --- a/packages/ui/core-components/src/lib/organisms/layout/sidebar/Sidebar.svelte +++ b/packages/ui/core-components/src/lib/organisms/layout/sidebar/Sidebar.svelte @@ -3,6 +3,7 @@ import { browser } from '$app/environment'; import { fly, fade, crossfade } from 'svelte/transition'; import { cubicInOut } from 'svelte/easing'; + import { config } from '$evidence/config'; import { lock, unlock } from 'tua-body-scroll-lock'; import { afterUpdate } from 'svelte'; @@ -133,7 +134,7 @@ {#each firstLevelFiles as file} {#if file.children.length === 0 && file.href && (file.frontMatter?.sidebar_link !== false || file.frontMatter?.sidebar_link === undefined)} - {@const active = $page.url.pathname.toUpperCase() === file.href.toUpperCase() + '/'} + {@const active = $page.url.pathname.replace(config.deployment.basePath, '').toUpperCase() === file.href.toUpperCase() + '/'} {#each firstLevelFiles as file} {#if file.children.length === 0 && file.href && (file.frontMatter?.sidebar_link !== false || file.frontMatter?.sidebar_link === undefined)} - {@const active = $page.url.pathname.toUpperCase() === file.href.toUpperCase() + '/'} + {@const active = $page.url.pathname.replace(config.deployment.basePath, '').toUpperCase() === file.href.toUpperCase() + '/'} From d54b4f6470f58d90a72b0f5801cca3d5a8f79b5c Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Tue, 23 Sep 2025 11:15:14 +0000 Subject: [PATCH 17/18] fixed pinging in remote environments --- packages/extension/src/commands/preview.ts | 20 ++++---- packages/extension/src/commands/server.ts | 48 +++++++++++++------ packages/extension/src/commands/settings.ts | 2 +- packages/extension/src/utils/httpUtils.ts | 44 +++++++++++------ .../lib/organisms/layout/BreadCrumbs.svelte | 2 +- 5 files changed, 77 insertions(+), 39 deletions(-) diff --git a/packages/extension/src/commands/preview.ts b/packages/extension/src/commands/preview.ts index d9ebfd40ea..30f318d351 100644 --- a/packages/extension/src/commands/preview.ts +++ b/packages/extension/src/commands/preview.ts @@ -47,16 +47,19 @@ export async function preview(uri?: Uri) { // from this point on, we know this is an Evidence project with a server running if (!uri || uri.path === '/') { // open the default app page in the built-in simple browser webview - const homePage: Uri = await getAppPageUri('/'); - // I suspect this to be causing the confusing message "please refresh preview" + const homePageUris = await getAppPageUri('/'); + // I suspect this to be causing the confusing message "Please reopen the preview" // openPageView(homePage); - // wait for the server to load the app home page - await waitFor(homePage.toString(true), 1000, 30000); // encoding, ms interval, max total wait time ms + // to know when the server is ready to serve the page, we ping the local + // uri of the page + await waitFor(homePageUris.internal.toString(true), + 500, 30_000); // encoding, ms interval, max total wait time ms - // call the built-in simple browser once more - // to load the home page content on server startup - openPageView(homePage); + // call the built-in simple browser once more to load the home page content + // on server startup, note that this is a browser running on the local machine + // and not the remote workspace environment. + openPageView(homePageUris.external); return; } @@ -90,10 +93,11 @@ export async function preview(uri?: Uri) { pageUrlPath = `/${pageUrlPath}`; // create external app page Uri from page url path - const pageUri: Uri = await getAppPageUri(pageUrlPath); + const pageUri: Uri = (await getAppPageUri(pageUrlPath)).external; // open requested page in the built-in simple browser webview openPageView(pageUri); + return; } window.showErrorMessage('Preview is not possible without a folder opened.'); diff --git a/packages/extension/src/commands/server.ts b/packages/extension/src/commands/server.ts index 875d2000fc..b9df8143da 100644 --- a/packages/extension/src/commands/server.ts +++ b/packages/extension/src/commands/server.ts @@ -1,10 +1,10 @@ import { window, commands, env, workspace, Uri, ViewColumn } from 'vscode'; import { Commands } from './commands'; -import { Context as ExtensionContext, Settings, getConfig, getWorkspaceFolder } from '../config'; +import { Context, Settings, getConfig, getWorkspaceFolder } from '../config'; import { getOutputChannel } from '../output'; import { closeTerminal, sendCommand } from '../terminal'; -import { localAppUrl, openPageView } from './preview'; +import { localAppUrl, openPageView, preview } from './preview'; import { getNodeVersion, isSupportedNodeVersion, promptToInstallNodeJsAndRestart } from '../node'; import { statusBar } from '../statusBar'; import { timeout } from '../utils/timer'; @@ -13,7 +13,6 @@ import { hasDependencies } from './build'; import { telemetryService } from '../extension'; import { hasManifest, getTypesFromConnections, getPackageJsonFolder } from '../utils/jsonUtils'; import { isProcessRunning } from '../utils/shellUtils'; -import { Context } from 'mocha'; const localhost = ['127.0.0.1', '::1', 'localhost']; // Formerly this was managed as a simple boolean variable which was problematic @@ -41,11 +40,16 @@ const setContext = (key: any, value: any) => { * @param pageUrlPath Optional, the path part of the app Url * to be converted to an external app Url * - * @returns External Url which can be safely opened in a browser - * on the local system even when vscode is running remotely (e.g. - * in code-server, using SSH, or in GitHub Codespaces) + * @returns An object that contains both the internal and the + * external Urls. The internal Url can be used by workspace + * actions such as pinging while the external Url is the one + * we can safely open in a browser on the local system when + * vscode is running at a remoteworkspace (e.g. using code- + * server, SSH, or in GitHub Codespaces) */ -export async function getAppPageUri(pageUrlPath?: string): Promise { +export async function getAppPageUri(pageUrlPath?: string): Promise<{ + internal: Uri, external: Uri} +> { const defaultPort = getConfig(Settings.DefaultPort); // update active server port number @@ -56,6 +60,7 @@ export async function getAppPageUri(pageUrlPath?: string): Promise { } const internalServerUrl = `${localAppUrl}:${_activePort ?? defaultPort}`; + let internalPageUri = Uri.parse(internalServerUrl); // get external web page url let externalPageUri: Uri = await env.asExternalUri(Uri.parse(internalServerUrl)); @@ -64,17 +69,30 @@ export async function getAppPageUri(pageUrlPath?: string): Promise { // external form, and discards the path part of the Url when present. Hence this // should be added after the convertion to an external Url. if (pageUrlPath?.startsWith('/')) { - // append page path to the external base path (e.g. /proxy/3710 + /another = /proxy/3710/another) + // append page path to the external base path + // (e.g. /proxy/3710 + /another = /proxy/3710/another) // handle potential trailing/leading slashes to avoid double slashes const basePath = externalPageUri.path.endsWith('/') ? externalPageUri.path.slice(0, -1) : externalPageUri.path; externalPageUri = externalPageUri.with({ path: basePath + pageUrlPath }); } + + // The assumption here is that EVIDENCE_BASE_PATH has been injected into the local dev + // server instance as well (check startServer). + internalPageUri = internalPageUri.with({ + path: externalPageUri.path + }); const outputChannel = getOutputChannel(); - outputChannel.appendLine(`Requested app page: ${externalPageUri.toString(true)}`); // skip encoding - - return externalPageUri; + outputChannel.appendLine(`Requested app page: ${externalPageUri} ${ + internalPageUri.toString(true) == externalPageUri.toString(true) ? + '' : `(this is ${internalPageUri.toString(true)} on the remote)` + }`); + + return { + internal: internalPageUri, + external: externalPageUri, + }; } /** @@ -94,7 +112,7 @@ export async function startServer(pageUri?: Uri) { telemetryService?.sendEvent('startServer'); if (!pageUri) { - pageUri = await getAppPageUri('/'); + pageUri = (await getAppPageUri('/')).external; } const pageHostname = pageUri.authority.split(':')[0]; @@ -185,7 +203,7 @@ export async function startServer(pageUri?: Uri) { // update server status and show running status bar icon statusBar.showRunning(); - setContext(ExtensionContext.IsServerRunning, true); + setContext(Context.IsServerRunning, true); if (previewType.includes('internal')) { // wait for the dev server to start @@ -205,7 +223,7 @@ export async function startServer(pageUri?: Uri) { commands.executeCommand(Commands.FocusActiveEditorGroup); // open app preview if previewType is set to internal (simple browser) - openPageView(pageUri); + preview(); // change button to stop server statusBar.showStop(); @@ -245,7 +263,7 @@ export async function stopServer() { closeTerminal(); // reset server state and status display - setContext(ExtensionContext.IsServerRunning, false); + setContext(Context.IsServerRunning, false); _activePort = getConfig(Settings.DefaultPort); statusBar.showStart(); telemetryService?.sendEvent('stopServer'); diff --git a/packages/extension/src/commands/settings.ts b/packages/extension/src/commands/settings.ts index 79cee25473..8384ec4efb 100644 --- a/packages/extension/src/commands/settings.ts +++ b/packages/extension/src/commands/settings.ts @@ -31,7 +31,7 @@ export async function viewExtensionSettings() { * @deprecated */ export async function viewAppSettings() { - const settingsPageUri: Uri = await getAppPageUri(settingsPagePath); + const settingsPageUri: Uri = (await getAppPageUri(settingsPagePath)).external; if (!(await isServerRunning())) { startServer(settingsPageUri); } else { diff --git a/packages/extension/src/utils/httpUtils.ts b/packages/extension/src/utils/httpUtils.ts index 55beb165b1..bffce6d6ce 100644 --- a/packages/extension/src/utils/httpUtils.ts +++ b/packages/extension/src/utils/httpUtils.ts @@ -47,7 +47,7 @@ export async function tryPort(port = 3000): Promise { * @param url The url to ping. * @returns Url ping result promise. */ -export function ping(url: string) { +export function ping(url: string, timeout: number = 1000) { const promise = new Promise((resolve) => { const useHttps = url.indexOf('https') === 0; const request = useHttps ? https.request : http.request; @@ -56,18 +56,34 @@ export function ping(url: string) { const outputChannel = getOutputChannel(); outputChannel.appendLine(`Pinging page: ${url}`); - const pingRequest = request(url, () => { - resolve(true); - pingRequest.destroy(); - }); + try { + const urlObj = new URL(url); + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (useHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method: 'HEAD', // HEAD is better for ping + timeout + }; - pingRequest.on('error', () => { - resolve(false); - pingRequest.destroy(); - }); + const pingRequest = request(options, (res) => { + resolve(true); // Any response means server is reachable + pingRequest.destroy(); + }); + + pingRequest.on('error', () => { + resolve(false); + }); - pingRequest.write(''); - pingRequest.end(); + pingRequest.on('timeout', () => { + resolve(false); + pingRequest.destroy(); + }); + + pingRequest.end(); + } catch (error) { + resolve(false); + } }); return promise; @@ -76,17 +92,17 @@ export function ping(url: string) { /** * Waits for a url to be pingable. * - * @param url The url to ping. + * @param url The (internal) url to ping. * @param interval The interval to wait between pings. * @param max The maximum amount of time to wait. * * @returns Url ping result promise. */ -export async function waitFor(url: string, interval = 200, max = 30_000) { +export async function waitFor(url: string, interval = 500, max = 30_000) { let time = Math.ceil(max / interval); while (time > 0) { time -= 1; - if (await ping(url)) { + if (await ping(url, interval)) { return true; } await timeout(interval); diff --git a/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte b/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte index e951e7dc37..0ab331dcdd 100644 --- a/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte +++ b/packages/ui/core-components/src/lib/organisms/layout/BreadCrumbs.svelte @@ -59,7 +59,7 @@ let crumbs = []; - $: crumbs = buildCrumbs($page.url.pathname.replace(config.deployment.basePath, '').split('/').slice(1), fileTree); + $: crumbs = buildCrumbs($page.url.pathname.replace(config.deployment.basePath || '', '').split('/').slice(1), fileTree);
From 2c5dead8ec4702993efdd1f1569663443295dca6 Mon Sep 17 00:00:00 2001 From: Orwa Diraneyya Date: Tue, 23 Sep 2025 11:38:19 +0000 Subject: [PATCH 18/18] added updated changeset --- .changeset/ninety-boats-brake.md | 8 ++++++++ .tool-versions | 2 ++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/ninety-boats-brake.md create mode 100644 .tool-versions diff --git a/.changeset/ninety-boats-brake.md b/.changeset/ninety-boats-brake.md new file mode 100644 index 0000000000..28063d098a --- /dev/null +++ b/.changeset/ninety-boats-brake.md @@ -0,0 +1,8 @@ +--- +'@evidence-dev/evidence': minor +'evidence-vscode': minor +'@evidence-dev/sdk': minor +'@evidence-dev/tailwind': patch +--- + +Added better support for remote environments including running in code-server with auto-port forwarding, also added a feature for disabling the automatic building of sources by the dev server diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..2fc7144419 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +nodejs 22.19.0 +pnpm 10.17.0