mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-19 09:51:59 +08:00
build(editor): Resolve frontend workspace packages from source in Vite (no-changelog) (#36131)
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
multica-agent
parent
f44bd4827f
commit
3beabfb66f
@@ -104,6 +104,7 @@ jobs:
|
||||
packages/frontend/editor-ui/vite.config.mts
|
||||
pnpm-workspace.yaml
|
||||
packages/@n8n/*/package.json
|
||||
packages/frontend/@n8n/frontend-vite-config/**
|
||||
packages/testing/playwright/tests/dev-server-smoke/**
|
||||
packages/testing/playwright/playwright.config.ts
|
||||
packages/testing/playwright/playwright-projects.ts
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { Alias } from 'vite';
|
||||
|
||||
import { frontendModuleAliases, frontendSourceAliases } from './source-packages.js';
|
||||
|
||||
/**
|
||||
* Packages reached *transitively*: they are not a declared dependency of anyone, so they cannot sit
|
||||
* in the source-packages table.
|
||||
*
|
||||
* `n8n-workflow`'s expression-sandboxing imports `astVisit` from `@n8n/tournament`, whose dist is
|
||||
* CJS. Linked workspace packages skip optimizeDeps, so the dev server serves that file verbatim and
|
||||
* the browser fails to parse a named export out of it; the build survives because rolldown interops
|
||||
* CJS, but pays ~397 kB in defeated tree-shaking. Resolving to src avoids both.
|
||||
*/
|
||||
export const transitiveWorkspaceAliases = (packagesDir: string): Alias[] => [
|
||||
{ find: '@n8n/tournament', replacement: resolve(packagesDir, '@n8n', 'tournament', 'src') },
|
||||
];
|
||||
|
||||
/**
|
||||
* Shell-only, deliberately out of `frontendAliases`: these replacements are bare specifiers
|
||||
* resolved from the consumer's own `node_modules`, and only editor-ui declares `lodash` and
|
||||
* `stream-browserify`. Shared with a module's vitest, a value import of `stream` anywhere in its
|
||||
* graph would fail to resolve — latent today only because `n8n-workflow` imports `stream` as a
|
||||
* type.
|
||||
*/
|
||||
export const vendorAliases = (): Alias[] => [
|
||||
...['orderBy', 'camelCase', 'cloneDeep', 'startCase'].map((name) => ({
|
||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||
replacement: `lodash/${name}`,
|
||||
})),
|
||||
{ find: /^lodash\.(.+)$/, replacement: 'lodash/$1' },
|
||||
// `n8n-workflow` reaches Node's `stream` in a browser graph.
|
||||
{ find: 'stream', replacement: 'stream-browserify' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Every entry rewrites to an absolute path under `packages/`, which is what makes the set safe to
|
||||
* share from any directory — `aliases.test.ts` holds that line.
|
||||
*/
|
||||
export const frontendAliases = (packagesDir: string): Alias[] => [
|
||||
...frontendSourceAliases(packagesDir),
|
||||
...transitiveWorkspaceAliases(packagesDir),
|
||||
];
|
||||
|
||||
/**
|
||||
* Order is resolution order: vendor rewrites stay last, where they sat before this package existed.
|
||||
*/
|
||||
export const shellAliases = (packagesDir: string): Alias[] => [
|
||||
...frontendAliases(packagesDir),
|
||||
...frontendModuleAliases(packagesDir),
|
||||
...vendorAliases(),
|
||||
];
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["../../../biome.jsonc"]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export {
|
||||
frontendAliases,
|
||||
shellAliases,
|
||||
transitiveWorkspaceAliases,
|
||||
vendorAliases,
|
||||
} from './aliases.js';
|
||||
|
||||
export {
|
||||
frontendModuleAliases,
|
||||
frontendSourceAliases,
|
||||
// The tables themselves, for the guard test that checks them against tsconfig `paths`.
|
||||
modulePackages,
|
||||
sourcePackages,
|
||||
} from './source-packages.js';
|
||||
@@ -1,68 +0,0 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { Alias } from 'vite';
|
||||
|
||||
/**
|
||||
* Workspace packages the frontend consumes from source rather than from `dist`, so that an edit in
|
||||
* one of them hot-reloads the editor without a rebuild. `dir` is relative to `packages/`.
|
||||
*
|
||||
* Must stay in step with editor-ui's tsconfig `paths` and with `tsconfig.frontend-module.json`:
|
||||
* when those disagreed, vue-tsc typechecked a package from `src` while the bundle was built from
|
||||
* its `dist`. `editor-ui/vite/aliases.test.ts` fails when they diverge and names what to update.
|
||||
*
|
||||
* It lives here rather than in editor-ui because every `packages/modules/<name>/frontend` needs the
|
||||
* same mapping for its own vitest run, and a module cannot import from the shell it plugs into.
|
||||
*
|
||||
* `entry: false` marks packages with no `src/index.ts`. Their `exports` map has no `.`, so a bare
|
||||
* import of them does not resolve at all and must not be aliased.
|
||||
*/
|
||||
export const sourcePackages = [
|
||||
{ name: '@n8n/api-types', dir: '@n8n/api-types' },
|
||||
{ name: '@n8n/chat', dir: 'frontend/@n8n/chat' },
|
||||
{ name: '@n8n/chat-hub', dir: '@n8n/chat-hub' },
|
||||
{ name: '@n8n/composables', dir: 'frontend/@n8n/composables', entry: false },
|
||||
{ name: '@n8n/constants', dir: '@n8n/constants' },
|
||||
{ name: '@n8n/design-system', dir: 'frontend/@n8n/design-system' },
|
||||
{ name: '@n8n/frontend-constants', dir: 'frontend/@n8n/frontend-constants', entry: false },
|
||||
{ name: '@n8n/frontend-module-sdk', dir: 'frontend/@n8n/frontend-module-sdk' },
|
||||
{ name: '@n8n/frontend-utils', dir: 'frontend/@n8n/frontend-utils', entry: false },
|
||||
{ name: '@n8n/i18n', dir: 'frontend/@n8n/i18n' },
|
||||
{ name: '@n8n/rest-api-client', dir: 'frontend/@n8n/rest-api-client' },
|
||||
{ name: '@n8n/stores', dir: 'frontend/@n8n/stores' },
|
||||
{ name: '@n8n/telemetry', dir: '@n8n/telemetry' },
|
||||
{ name: '@n8n/utils', dir: '@n8n/utils', entry: false },
|
||||
];
|
||||
|
||||
/**
|
||||
* Feature module packages, appended by `n8n-module-sdk create`. Kept separate from the table above
|
||||
* because only the shell may resolve them: a module aliasing its siblings would let an accidental
|
||||
* cross-module import resolve at test time, which is the boundary the module tsconfig base holds.
|
||||
*/
|
||||
export const modulePackages: Array<{ name: string; dir: string; entry?: boolean }> = [];
|
||||
|
||||
const escapeForRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Each package gets a slash-delimited, anchored pair — `^@n8n/chat$` and `^@n8n/chat/(.+)$`. One
|
||||
* open-ended `^@n8n/chat(.+)$` would match `@n8n/chat-hub/…` as well, and would leave the bare
|
||||
* `@n8n/chat` unmatched — `(.+)` needs a character after the package name — falling through to
|
||||
* `dist`.
|
||||
*/
|
||||
const expand = (packagesDir: string, packages: typeof modulePackages): Alias[] =>
|
||||
packages.flatMap(({ name, dir, entry = true }) => {
|
||||
const src = resolve(packagesDir, dir, 'src');
|
||||
const pattern = escapeForRegExp(name);
|
||||
|
||||
return [
|
||||
...(entry
|
||||
? [{ find: new RegExp(`^${pattern}$`), replacement: resolve(src, 'index.ts') }]
|
||||
: []),
|
||||
{ find: new RegExp(`^${pattern}/(.+)$`), replacement: `${src}/$1` },
|
||||
];
|
||||
});
|
||||
|
||||
export const frontendSourceAliases = (packagesDir: string): Alias[] =>
|
||||
expand(packagesDir, sourcePackages);
|
||||
|
||||
/** Shell-only — see `modulePackages`. */
|
||||
export const frontendModuleAliases = (packagesDir: string): Alias[] =>
|
||||
expand(packagesDir, modulePackages);
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": ["./tsconfig.json", "@n8n/typescript-config/tsconfig.build.go.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.go.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["../../../../biome.jsonc"]
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { Alias } from 'vite';
|
||||
|
||||
// Edit the two tables that follow. Keep the tables at the top of this file.
|
||||
|
||||
/**
|
||||
* The frontend reads these workspace packages from source, not from `dist`. An edit in one of them
|
||||
* hot-reloads the editor with no rebuild. `dir` is relative to `packages/`.
|
||||
*
|
||||
* Keep this table in step with the `paths` in `editor-ui/tsconfig.json` and in
|
||||
* `tsconfig.frontend-module.json`. When they disagreed, vue-tsc read a package from `src`, but the
|
||||
* bundle used the `dist` of that package. `editor-ui/vite/aliases.test.ts` fails when they
|
||||
* disagree. The test names the file to correct.
|
||||
*
|
||||
* This table stays here, not in editor-ui. Each `packages/modules/<name>/frontend` needs the same
|
||||
* map for its own vitest run. A module must not import from the shell.
|
||||
*
|
||||
* `entry: false` marks a package with no `src/index.ts`. The `exports` map of such a package has no
|
||||
* `.` key. A bare import of it does not resolve, so do not make an alias for it.
|
||||
*/
|
||||
export const sourcePackages = [
|
||||
{ name: '@n8n/api-types', dir: '@n8n/api-types' },
|
||||
{ name: '@n8n/chat', dir: 'frontend/@n8n/chat' },
|
||||
{ name: '@n8n/chat-hub', dir: '@n8n/chat-hub' },
|
||||
{ name: '@n8n/composables', dir: 'frontend/@n8n/composables', entry: false },
|
||||
{ name: '@n8n/constants', dir: '@n8n/constants' },
|
||||
{ name: '@n8n/design-system', dir: 'frontend/@n8n/design-system' },
|
||||
{ name: '@n8n/frontend-constants', dir: 'frontend/@n8n/frontend-constants', entry: false },
|
||||
{ name: '@n8n/frontend-module-sdk', dir: 'frontend/@n8n/frontend-module-sdk' },
|
||||
{ name: '@n8n/frontend-utils', dir: 'frontend/@n8n/frontend-utils', entry: false },
|
||||
{ name: '@n8n/i18n', dir: 'frontend/@n8n/i18n' },
|
||||
{ name: '@n8n/rest-api-client', dir: 'frontend/@n8n/rest-api-client' },
|
||||
{ name: '@n8n/stores', dir: 'frontend/@n8n/stores' },
|
||||
{ name: '@n8n/telemetry', dir: '@n8n/telemetry' },
|
||||
{ name: '@n8n/utils', dir: '@n8n/utils', entry: false },
|
||||
];
|
||||
|
||||
/**
|
||||
* `n8n-module-sdk create` adds a feature module package to this table. Only the shell resolves
|
||||
* these packages, so they stay out of the table above.
|
||||
*
|
||||
* A module that aliases the other modules lets a cross-module import resolve in its own test run.
|
||||
* The module tsconfig base holds that boundary.
|
||||
*/
|
||||
export const modulePackages: Array<{ name: string; dir: string; entry?: boolean }> = [];
|
||||
|
||||
// The code below makes the Vite aliases from the two tables. Keep this code in the same file as
|
||||
// the tables. A second file needs an import with a `.ts` specifier. That import causes error
|
||||
// TS5097 in each package that imports this one.
|
||||
|
||||
const escapeForRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Each package gets two anchored patterns: `^@n8n/chat$` and `^@n8n/chat/(.+)$`. The slash keeps
|
||||
* them apart.
|
||||
*
|
||||
* One open pattern `^@n8n/chat(.+)$` also matches `@n8n/chat-hub/…`. It does not match the bare
|
||||
* `@n8n/chat`, because `(.+)` needs one more character after the name. That bare import then
|
||||
* resolves to `dist`.
|
||||
*/
|
||||
const expand = (packagesDir: string, packages: typeof modulePackages): Alias[] =>
|
||||
packages.flatMap(({ name, dir, entry = true }) => {
|
||||
const src = resolve(packagesDir, dir, 'src');
|
||||
const pattern = escapeForRegExp(name);
|
||||
|
||||
return [
|
||||
...(entry
|
||||
? [{ find: new RegExp(`^${pattern}$`), replacement: resolve(src, 'index.ts') }]
|
||||
: []),
|
||||
{ find: new RegExp(`^${pattern}/(.+)$`), replacement: `${src}/$1` },
|
||||
];
|
||||
});
|
||||
|
||||
export const frontendSourceAliases = (packagesDir: string): Alias[] =>
|
||||
expand(packagesDir, sourcePackages);
|
||||
|
||||
/** Only the shell uses this map. See `modulePackages`. */
|
||||
export const frontendModuleAliases = (packagesDir: string): Alias[] =>
|
||||
expand(packagesDir, modulePackages);
|
||||
|
||||
/**
|
||||
* `n8n-workflow` imports `astVisit` from `@n8n/tournament` for its expression sandbox. Nothing
|
||||
* declares `@n8n/tournament` as a dependency, so it cannot go in the table above.
|
||||
*
|
||||
* The `dist` of `@n8n/tournament` is CJS. Vite skips optimizeDeps for a linked workspace package,
|
||||
* so the dev server sends that file as it is. The browser then fails to parse a named export from
|
||||
* it.
|
||||
*
|
||||
* The production build works, because rolldown reads CJS. But the build loses tree-shaking and
|
||||
* adds approximately 397 kB. An alias to `src` prevents both problems.
|
||||
*/
|
||||
export const transitiveWorkspaceAliases = (packagesDir: string): Alias[] => [
|
||||
{ find: '@n8n/tournament', replacement: resolve(packagesDir, '@n8n', 'tournament', 'src') },
|
||||
];
|
||||
|
||||
/**
|
||||
* Only the shell uses these rewrites, so they stay out of `frontendAliases`. Each one points to a
|
||||
* bare specifier, which resolves from the `node_modules` of the consumer. Only editor-ui declares
|
||||
* `lodash` and `stream-browserify`.
|
||||
*
|
||||
* If a module vitest run used this map, a value import of `stream` in its graph would not resolve.
|
||||
* The problem is hidden today, because `n8n-workflow` imports `stream` as a type only.
|
||||
*/
|
||||
export const vendorAliases = (): Alias[] => [
|
||||
...['orderBy', 'camelCase', 'cloneDeep', 'startCase'].map((name) => ({
|
||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||
replacement: `lodash/${name}`,
|
||||
})),
|
||||
{ find: /^lodash\.(.+)$/, replacement: 'lodash/$1' },
|
||||
// `n8n-workflow` uses the `stream` module of Node in a browser graph.
|
||||
{ find: 'stream', replacement: 'stream-browserify' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Each entry points to an absolute path under `packages/`. A consumer in any directory can use this
|
||||
* set, because the paths are absolute. `aliases.test.ts` tests that rule.
|
||||
*/
|
||||
export const frontendAliases = (packagesDir: string): Alias[] => [
|
||||
...frontendSourceAliases(packagesDir),
|
||||
...transitiveWorkspaceAliases(packagesDir),
|
||||
];
|
||||
|
||||
/**
|
||||
* Vite uses the first match, so the order here is the resolution order. The vendor rewrites stay
|
||||
* last, where they were before this package existed.
|
||||
*/
|
||||
export const shellAliases = (packagesDir: string): Alias[] => [
|
||||
...frontendAliases(packagesDir),
|
||||
...frontendModuleAliases(packagesDir),
|
||||
...vendorAliases(),
|
||||
];
|
||||
+4
-11
@@ -11,22 +11,15 @@
|
||||
"typescript": "catalog:typescript",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"main": "index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
".": "./index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"clean": "rimraf .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build:unchecked": "tsc -p tsconfig.build.json --noCheck",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"watch": "tsc -p tsconfig.build.json --watch"
|
||||
"format:check": "biome ci ."
|
||||
},
|
||||
"license": "LicenseRef-n8n-sustainable-use"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.common.go.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
// The frontend reads this package from source. No command must emit a `.js` file here.
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
@@ -135,6 +135,7 @@
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@iconify/json": "catalog:",
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/frontend-vite-config": "workspace:*",
|
||||
"@n8n/playwright-janitor": "workspace:*",
|
||||
"@n8n/stylelint-config": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Each import in this file must resolve with no build step.
|
||||
// Put an import that needs a `dist` in `vitest.config.mts`.
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig, mergeConfig, type UserConfig } from 'vite';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import { codecovVitePlugin } from '@codecov/vite-plugin';
|
||||
|
||||
import { vitestConfig } from '@n8n/vitest-config/frontend';
|
||||
import icons from 'unplugin-icons/vite';
|
||||
import { lucideIconsPlugin } from '../@n8n/design-system/src/icons/lucide/vite';
|
||||
import browserslistToEsbuild from 'browserslist-to-esbuild';
|
||||
@@ -14,6 +15,7 @@ import legacy from '@vitejs/plugin-legacy';
|
||||
import browserslist from 'browserslist';
|
||||
import { isLocaleFile, sendLocaleUpdate } from './vite/i18n-locales-hmr-helpers';
|
||||
import { nodePopularityPlugin } from './vite/vite-plugin-node-popularity.mjs';
|
||||
import { editorUiAliases } from './vite/aliases.mjs';
|
||||
|
||||
const publicPath = process.env.VUE_APP_PUBLIC_PATH || '/';
|
||||
|
||||
@@ -27,84 +29,7 @@ const packagesDir = resolve(__dirname, '..', '..');
|
||||
// Vite resolves it to a single copy. The other curated libs are backend-only.
|
||||
const singleInstanceDedupe = ['zod'];
|
||||
|
||||
const alias = [
|
||||
{ find: '@', replacement: resolve(__dirname, 'src') },
|
||||
{ find: 'stream', replacement: 'stream-browserify' },
|
||||
// Stub out @n8n/expression-runtime for browser build (it pulls in isolated-vm, a Node.js-only native module)
|
||||
{
|
||||
find: '@n8n/expression-runtime',
|
||||
replacement: resolve(__dirname, 'vite/expression-runtime-stub.ts'),
|
||||
},
|
||||
// Ensure bare imports resolve to sources (not dist)
|
||||
{ find: '@n8n/i18n', replacement: resolve(packagesDir, 'frontend', '@n8n', 'i18n', 'src') },
|
||||
{ find: '@n8n/chat-hub', replacement: resolve(packagesDir, '@n8n', 'chat-hub', 'src') },
|
||||
{ find: '@n8n/tournament', replacement: resolve(packagesDir, '@n8n', 'tournament', 'src') },
|
||||
{
|
||||
find: /^@n8n\/chat(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'chat', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/chat-hub(.+)$/,
|
||||
replacement: resolve(packagesDir, '@n8n', 'chat-hub', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/api-requests(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'api-requests', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/composables(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'composables', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/frontend-module-sdk$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'frontend-module-sdk', 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/constants(.+)$/,
|
||||
replacement: resolve(packagesDir, '@n8n', 'constants', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/design-system$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'design-system', 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/design-system(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'design-system', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/i18n(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'i18n', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/stores(.+)$/,
|
||||
replacement: resolve(packagesDir, 'frontend', '@n8n', 'stores', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/telemetry$/,
|
||||
replacement: resolve(packagesDir, '@n8n', 'telemetry', 'src/index.ts'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/telemetry(.+)$/,
|
||||
replacement: resolve(packagesDir, '@n8n', 'telemetry', 'src$1'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/utils(.+)$/,
|
||||
replacement: resolve(packagesDir, '@n8n', 'utils', 'src$1'),
|
||||
},
|
||||
...['orderBy', 'camelCase', 'cloneDeep', 'startCase'].map((name) => ({
|
||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||
replacement: `lodash/${name}`,
|
||||
})),
|
||||
{
|
||||
find: /^lodash\.(.+)$/,
|
||||
replacement: 'lodash/$1',
|
||||
},
|
||||
{
|
||||
// For sanitize-html
|
||||
find: 'source-map-js',
|
||||
replacement: resolve(__dirname, 'vite/source-map-js-shim'),
|
||||
},
|
||||
];
|
||||
const alias = editorUiAliases(__dirname, packagesDir);
|
||||
|
||||
const { RELEASE: release } = process.env;
|
||||
|
||||
@@ -229,44 +154,41 @@ const plugins: UserConfig['plugins'] = [
|
||||
|
||||
const target = browserslistToEsbuild(browsers);
|
||||
|
||||
export default mergeConfig(
|
||||
defineConfig({
|
||||
define: {
|
||||
// This causes test to fail but is required for actually running it
|
||||
// ...(NODE_ENV !== 'test' ? { 'global': 'globalThis' } : {}),
|
||||
...(NODE_ENV === 'development' ? { 'process.env': {} } : {}),
|
||||
BASE_PATH: `'${publicPath}'`,
|
||||
},
|
||||
plugins,
|
||||
resolve: { alias, dedupe: singleInstanceDedupe },
|
||||
base: publicPath,
|
||||
envPrefix: ['VUE', 'N8N_ENV_FEAT'],
|
||||
css: {
|
||||
preprocessorMaxWorkers: 2,
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: [
|
||||
'',
|
||||
'@use "@/app/css/_variables.scss" as *;',
|
||||
'@use "@n8n/design-system/css/mixins" as mixins;',
|
||||
].join('\n'),
|
||||
},
|
||||
export default defineConfig({
|
||||
define: {
|
||||
// This causes test to fail but is required for actually running it
|
||||
// ...(NODE_ENV !== 'test' ? { 'global': 'globalThis' } : {}),
|
||||
...(NODE_ENV === 'development' ? { 'process.env': {} } : {}),
|
||||
BASE_PATH: `'${publicPath}'`,
|
||||
},
|
||||
plugins,
|
||||
resolve: { alias, dedupe: singleInstanceDedupe },
|
||||
base: publicPath,
|
||||
envPrefix: ['VUE', 'N8N_ENV_FEAT'],
|
||||
css: {
|
||||
preprocessorMaxWorkers: 2,
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: [
|
||||
'',
|
||||
'@use "@/app/css/_variables.scss" as *;',
|
||||
'@use "@n8n/design-system/css/mixins" as mixins;',
|
||||
].join('\n'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
minify: !!release,
|
||||
// Coverage builds emit INLINE maps so browser V8 coverage carries the
|
||||
// map in the script source and monocart resolves offsets back to src.
|
||||
sourcemap: process.env.BUILD_WITH_COVERAGE === 'true' ? 'inline' : !!release,
|
||||
target,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['wa-sqlite'],
|
||||
rolldownOptions: {},
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
}),
|
||||
vitestConfig,
|
||||
);
|
||||
},
|
||||
build: {
|
||||
minify: !!release,
|
||||
// Coverage builds emit INLINE maps so browser V8 coverage carries the
|
||||
// map in the script source and monocart resolves offsets back to src.
|
||||
sourcemap: process.env.BUILD_WITH_COVERAGE === 'true' ? 'inline' : !!release,
|
||||
target,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['wa-sqlite'],
|
||||
rolldownOptions: {},
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { resolve } from 'path';
|
||||
import type { Alias } from 'vite';
|
||||
|
||||
// `@n8n/frontend-vite-config` holds the part that modules also use. Each module needs the same
|
||||
// map for its own vitest run. A module must not import from the shell.
|
||||
import { shellAliases } from '@n8n/frontend-vite-config';
|
||||
|
||||
export const appAliases = (editorUiDir: string): Alias[] => [
|
||||
{ find: '@', replacement: resolve(editorUiDir, 'src') },
|
||||
// Stub out @n8n/expression-runtime for browser build (it pulls in isolated-vm, a Node.js-only native module)
|
||||
{
|
||||
find: '@n8n/expression-runtime',
|
||||
replacement: resolve(editorUiDir, 'vite/expression-runtime-stub.ts'),
|
||||
},
|
||||
{
|
||||
// For sanitize-html
|
||||
find: 'source-map-js',
|
||||
replacement: resolve(editorUiDir, 'vite/source-map-js-shim'),
|
||||
},
|
||||
];
|
||||
|
||||
export const editorUiAliases = (editorUiDir: string, packagesDir: string): Alias[] => [
|
||||
...appAliases(editorUiDir),
|
||||
...shellAliases(packagesDir),
|
||||
];
|
||||
@@ -0,0 +1,142 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import type { Alias } from 'vite';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { frontendAliases, modulePackages, sourcePackages } from '@n8n/frontend-vite-config';
|
||||
|
||||
import { editorUiAliases } from './aliases.mjs';
|
||||
|
||||
// vitest sets the cwd to the package root. Under jsdom, `import.meta.url` is not a file URL.
|
||||
const editorUiDir = process.cwd();
|
||||
const packagesDir = resolve(editorUiDir, '..', '..');
|
||||
const repoRoot = resolve(packagesDir, '..');
|
||||
|
||||
const MODULE_TSCONFIG = join(repoRoot, 'packages', '@n8n', 'typescript-config');
|
||||
|
||||
/** A tsconfig file is JSONC. The frontend ones carry only whole-line `//` comments. */
|
||||
const readTsconfig = (file: string) =>
|
||||
JSON.parse(
|
||||
readFileSync(file, 'utf8')
|
||||
.split('\n')
|
||||
.filter((line) => !line.trim().startsWith('//'))
|
||||
.join('\n'),
|
||||
) as { compilerOptions?: { paths?: Record<string, string[]> } };
|
||||
|
||||
/** This function makes the short `"@n8n/x*"` form equal to the `"@n8n/x"` plus `"@n8n/x/*"` pair. */
|
||||
const pathsByPackage = (file: string) => {
|
||||
const paths = readTsconfig(file).compilerOptions?.paths ?? {};
|
||||
const byPackage = new Map<string, string>();
|
||||
|
||||
for (const [key, [target]] of Object.entries(paths)) {
|
||||
const name = key.replace(/\/?\*$/, '');
|
||||
if (!name.startsWith('@n8n/')) continue;
|
||||
|
||||
const resolved = resolve(dirname(file), target.replace(/\/?\*$/, ''));
|
||||
byPackage.set(name, resolved.endsWith('.ts') ? dirname(resolved) : resolved);
|
||||
}
|
||||
|
||||
return byPackage;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function copies the resolve plugin of vite. The first match wins. The function then calls
|
||||
* `String.replace`.
|
||||
*
|
||||
* If no pattern matches, node resolution takes the specifier. Node resolution finds the built
|
||||
* `dist` of the package.
|
||||
*/
|
||||
const resolveSpecifier = (specifier: string, aliases: Alias[]): string => {
|
||||
const matched = aliases.find(({ find }) =>
|
||||
typeof find === 'string'
|
||||
? specifier === find || specifier.startsWith(`${find}/`)
|
||||
: find.test(specifier),
|
||||
);
|
||||
|
||||
if (!matched) return 'dist';
|
||||
|
||||
const target = specifier.replace(matched.find, matched.replacement);
|
||||
|
||||
// A directory target and its `index.ts` are the same module. Only the text is different.
|
||||
const asIndex = join(target, 'index.ts');
|
||||
return relative(repoRoot, existsSync(asIndex) ? asIndex : target);
|
||||
};
|
||||
|
||||
describe('editor-ui vite aliases', () => {
|
||||
const aliases = editorUiAliases(editorUiDir, packagesDir);
|
||||
const editorUiPaths = pathsByPackage(join(editorUiDir, 'tsconfig.json'));
|
||||
|
||||
// This is a real failure. For months, four packages used `src` for the typecheck and `dist`
|
||||
// for the build.
|
||||
it.each([...sourcePackages, ...modulePackages])(
|
||||
'resolves $name to the same src as tsconfig does',
|
||||
({ name, dir }) => {
|
||||
const srcDir = resolve(packagesDir, dir, 'src');
|
||||
const src = relative(repoRoot, srcDir);
|
||||
|
||||
expect(resolveSpecifier(`${name}/probe`, aliases)).toBe(`${src}/probe`);
|
||||
expect(editorUiPaths.get(name)).toBe(srcDir);
|
||||
},
|
||||
);
|
||||
|
||||
it('aliases every source package editor-ui typechecks from src', () => {
|
||||
const aliased = new Set([...sourcePackages, ...modulePackages].map(({ name }) => name));
|
||||
const pathed = [...editorUiPaths.keys()]
|
||||
// This is the browser stub of editor-ui. It is not a package that the frontend reads from
|
||||
// source.
|
||||
.filter((name) => name !== '@n8n/expression-runtime');
|
||||
|
||||
expect(pathed.filter((name) => !aliased.has(name))).toEqual([]);
|
||||
});
|
||||
|
||||
it('agrees with the shared module tsconfig base', () => {
|
||||
// If the two files disagree, the typecheck of a module uses a `src` that the editor never
|
||||
// bundles.
|
||||
const modulePaths = pathsByPackage(join(MODULE_TSCONFIG, 'tsconfig.frontend-module.json'));
|
||||
|
||||
for (const [name, srcDir] of modulePaths) {
|
||||
expect({ name, srcDir }).toEqual({ name, srcDir: editorUiPaths.get(name) });
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves @n8n/tournament from source', () => {
|
||||
// `n8n-workflow` brings in this package. Nothing declares it, so it is not in
|
||||
// `sourcePackages`. Its `dist` is CJS. The dev server gives a parse error for a named export.
|
||||
// The build loses tree-shaking and adds approximately 397 kB.
|
||||
expect(resolveSpecifier('@n8n/tournament', aliases)).toBe(
|
||||
'packages/@n8n/tournament/src/index.ts',
|
||||
);
|
||||
expect(resolveSpecifier('@n8n/tournament/ast', aliases)).toBe(
|
||||
'packages/@n8n/tournament/src/ast',
|
||||
);
|
||||
});
|
||||
|
||||
it('still applies the vendor rewrites the shared set drops', () => {
|
||||
const rewrite = (specifier: string) => {
|
||||
const matched = aliases.find(({ find }) =>
|
||||
typeof find === 'string' ? specifier === find : find.test(specifier),
|
||||
);
|
||||
return matched ? specifier.replace(matched.find, matched.replacement) : 'unmatched';
|
||||
};
|
||||
|
||||
expect(rewrite('stream')).toBe('stream-browserify');
|
||||
expect(rewrite('lodash.camelCase')).toBe('lodash/camelCase');
|
||||
});
|
||||
|
||||
it('shares only aliases that resolve to workspace source', () => {
|
||||
// Each `packages/modules/*/frontend` vitest run uses this set. The `node_modules` of a module
|
||||
// is not the `node_modules` of the shell. A rewrite to a bare specifier that only editor-ui
|
||||
// declares fails there.
|
||||
const shared = frontendAliases(packagesDir).map(({ replacement }) => replacement);
|
||||
|
||||
expect(shared.filter((replacement) => !replacement.startsWith(packagesDir))).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a package and its subpaths independently of entry order', () => {
|
||||
// One open pattern `^@n8n/chat(.+)$` also matches `@n8n/chat-hub/…`.
|
||||
expect(resolveSpecifier('@n8n/chat-hub/api', aliases)).toBe('packages/@n8n/chat-hub/src/api');
|
||||
expect(resolveSpecifier('@n8n/chat-hub/api', [...aliases].reverse())).toBe(
|
||||
'packages/@n8n/chat-hub/src/api',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { vitestConfig } from '@n8n/vitest-config/frontend';
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
|
||||
import viteConfig from './vite.config.mjs';
|
||||
|
||||
// This file is separate from `vite.config.mts`, because `@n8n/vitest-config` resolves to its
|
||||
// `dist`. Only `test` always has that `dist`, because turbo builds the dependencies before `test`.
|
||||
export default mergeConfig(viteConfig, vitestConfig);
|
||||
Generated
+15
-12
@@ -2583,18 +2583,6 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.3(zod@3.25.76)
|
||||
|
||||
packages/@n8n/frontend-vite-config:
|
||||
devDependencies:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
typescript:
|
||||
specifier: catalog:typescript
|
||||
version: 7.0.2
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
|
||||
|
||||
packages/@n8n/imap:
|
||||
dependencies:
|
||||
iconv-lite:
|
||||
@@ -5508,6 +5496,18 @@ importers:
|
||||
specifier: ^2.2.8
|
||||
version: 2.2.8(patch_hash=e2aee939ccac8a57fe449bfd92bedd8117841579526217bc39aca26c6b8c317f)(typescript@6.0.2)
|
||||
|
||||
packages/frontend/@n8n/frontend-vite-config:
|
||||
devDependencies:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../../@n8n/typescript-config
|
||||
typescript:
|
||||
specifier: catalog:typescript
|
||||
version: 7.0.2
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@20.19.41)(esbuild@0.28.1)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
|
||||
|
||||
packages/frontend/@n8n/i18n:
|
||||
dependencies:
|
||||
n8n-workflow:
|
||||
@@ -6157,6 +6157,9 @@ importers:
|
||||
'@n8n/eslint-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../@n8n/eslint-config
|
||||
'@n8n/frontend-vite-config':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/frontend-vite-config
|
||||
'@n8n/playwright-janitor':
|
||||
specifier: workspace:*
|
||||
version: link:../../testing/janitor
|
||||
|
||||
Reference in New Issue
Block a user