Skip to content

Commit e0d8ce2

Browse files
authored
Merge pull request #7784 from QwikDev/v2-throw-duplicate
chore: warn when importing core twice
2 parents 831ad08 + 0cdbd6d commit e0d8ce2

File tree

9 files changed

+118
-120
lines changed

9 files changed

+118
-120
lines changed

.changeset/lemon-dingos-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik': minor
3+
---
4+
5+
BREAKING: (slightly) Qwik will no longer scan all modules at build start to detect Qwik modules. Instead, a runtime check is done to prevent duplicate core imports. If you get a runtime error, you need to fix your build settings so they don't externalize qwik-related libraries.

packages/docs/src/repl/worker/repl-dependencies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ const _loadDependencies = async (replOptions: ReplInputOptions) => {
7777
}
7878

7979
if (!isSameQwikVersion(self.qwikCore?.version)) {
80+
// Special case: we allow importing qwik again in the same process.
81+
delete (globalThis as any).__qwik;
8082
await exec(QWIK_PKG_NAME, '/core.cjs');
8183
if (self.qwikCore) {
8284
console.debug(`Loaded ${QWIK_PKG_NAME}: ${self.qwikCore.version}`);

packages/docs/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export default defineConfig(async () => {
137137
'algoliasearch',
138138
'@algolia/autocomplete-core/dist/esm/reshape',
139139
'algoliasearch/dist/algoliasearch-lite.esm.browser',
140+
'qwik-image',
141+
'@modular-forms/qwik',
142+
'@qwik-ui/headless',
140143
],
141144
},
142145

packages/qwik-router/src/ssg/worker-thread.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import type {
1414
} from './types';
1515

1616
export async function workerThread(sys: System) {
17+
// Special case: we allow importing qwik again in the same process, it's ok because we just needed the serializer
18+
// TODO: remove this once we have vite environment API and no longer need the serializer separately
19+
delete (globalThis as any).__qwik;
1720
const ssgOpts = sys.getOptions();
1821
const pendingPromises = new Set<Promise<any>>();
1922

@@ -42,6 +45,9 @@ export async function workerThread(sys: System) {
4245
}
4346

4447
export async function createSingleThreadWorker(sys: System) {
48+
// Special case: we allow importing qwik again in the same process, it's ok because we just needed the serializer
49+
// TODO: remove this once we have vite environment API and no longer need the serializer separately
50+
delete (globalThis as any).__qwik;
4551
const ssgOpts = sys.getOptions();
4652
const pendingPromises = new Set<Promise<any>>();
4753

packages/qwik/src/core/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
//////////////////////////////////////////////////////////////////////////////////////////
2+
// Protect against duplicate imports
3+
//////////////////////////////////////////////////////////////////////////////////////////
4+
import { version } from './version';
5+
if ((globalThis as any).__qwik) {
6+
console.error(
7+
`==============================================\n` +
8+
`Qwik version ${(globalThis as any).__qwik} already imported while importing ${version}. Verify external vs bundled imports etc. This can lead to issues due to duplicated shared structures.\n` +
9+
`==============================================\n`
10+
);
11+
}
12+
(globalThis as any).__qwik = version;
13+
114
//////////////////////////////////////////////////////////////////////////////////////////
215
// Developer Core API
316
//////////////////////////////////////////////////////////////////////////////////////////

packages/qwik/src/optimizer/src/plugins/vite.ts

Lines changed: 83 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type {
55
GlobalInjections,
66
Optimizer,
77
OptimizerOptions,
8-
OptimizerSystem,
98
QwikManifest,
109
TransformModule,
1110
} from '../types';
@@ -25,7 +24,6 @@ import {
2524
type NormalizedQwikPluginOptions,
2625
type QwikBuildMode,
2726
type QwikBuildTarget,
28-
type QwikPackages,
2927
type QwikPluginOptions,
3028
} from './plugin';
3129
import { createRollupError, normalizeRollupOutputOptions } from './rollup';
@@ -96,7 +94,6 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
9694
async config(viteConfig, viteEnv) {
9795
await qwikPlugin.init();
9896

99-
const sys = qwikPlugin.getSys();
10097
const path = qwikPlugin.getPath();
10198

10299
let target: QwikBuildTarget;
@@ -149,8 +146,6 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
149146
if (input && typeof input === 'string') {
150147
input = [input];
151148
}
152-
const shouldFindVendors =
153-
!qwikViteOpts.disableVendorScan && (target !== 'lib' || viteCommand === 'serve');
154149
viteAssetsDir = viteConfig.build?.assetsDir;
155150
const useAssetsDir = target === 'client' && !!viteAssetsDir && viteAssetsDir !== '_astro';
156151
const pluginOpts: QwikPluginOptions = {
@@ -208,8 +203,6 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
208203
clientDevInput = qwikPlugin.normalizePath(clientDevInput);
209204
}
210205

211-
const vendorRoots = shouldFindVendors ? await findQwikRoots(sys, sys.cwd()) : [];
212-
const vendorIds = vendorRoots.map((v) => v.id);
213206
const isDevelopment = buildMode === 'development';
214207
const qDevKey = 'globalThis.qDev';
215208
const qTestKey = 'globalThis.qTest';
@@ -221,17 +214,11 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
221214

222215
const updatedViteConfig: UserConfig = {
223216
ssr: {
224-
noExternal: [
225-
QWIK_CORE_ID,
226-
QWIK_CORE_INTERNAL_ID,
227-
QWIK_CORE_SERVER,
228-
QWIK_BUILD_ID,
229-
...vendorIds,
230-
],
217+
noExternal: [QWIK_CORE_ID, QWIK_CORE_INTERNAL_ID, QWIK_CORE_SERVER, QWIK_BUILD_ID],
231218
},
232219
envPrefix: ['VITE_', 'PUBLIC_'],
233220
resolve: {
234-
dedupe: [...DEDUPE, ...vendorIds],
221+
dedupe: [...DEDUPE],
235222
conditions: buildMode === 'production' && target === 'client' ? ['min'] : [],
236223
alias: {
237224
'@builder.io/qwik': '@qwik.dev/core',
@@ -264,8 +251,6 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
264251
QWIK_JSX_DEV_RUNTIME_ID,
265252
QWIK_BUILD_ID,
266253
QWIK_CLIENT_MANIFEST_ID,
267-
// Sadly we can't specify **/*.qwik.*, so we need to specify each one
268-
...vendorIds,
269254
// v1 imports, they are removed during transform but vite doesn't know that
270255
'@builder.io/qwik',
271256
'@builder.io/qwik-city',
@@ -653,7 +638,87 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any {
653638
},
654639
} as const satisfies VitePlugin<QwikVitePluginApi>;
655640

656-
return [vitePluginPre, vitePluginPost];
641+
return [vitePluginPre, vitePluginPost, checkExternals()];
642+
}
643+
644+
/**
645+
* This plugin checks for external dependencies that should be included in the server bundle,
646+
* because they use Qwik. If they are not included, the optimizer won't process them, and there will
647+
* be two instances of Qwik Core loaded.
648+
*/
649+
async function checkExternals() {
650+
let fs: typeof import('fs').promises;
651+
let path: typeof import('path');
652+
try {
653+
fs = await import('node:fs').then((m) => m.promises);
654+
path = await import('node:path');
655+
} catch {
656+
// We can't do anything if we can't import fs and path
657+
return;
658+
}
659+
const seen: Set<string> = new Set();
660+
let rootDir: string;
661+
const core2 = '@qwik-dev/core';
662+
const core1 = '@builder.io/qwik';
663+
async function isQwikDep(dep: string, dir: string) {
664+
while (dir) {
665+
const pkg = path.join(dir, 'node_modules', dep, 'package.json');
666+
try {
667+
await fs.access(pkg);
668+
const data = await fs.readFile(pkg, {
669+
encoding: 'utf-8',
670+
});
671+
// any mention of lowercase qwik in the package.json is enough
672+
const json = JSON.parse(data);
673+
if (
674+
json.qwik ||
675+
json.dependencies?.[core2] ||
676+
json.peerDependencies?.[core2] ||
677+
json.dependencies?.[core1] ||
678+
json.peerDependencies?.[core1]
679+
) {
680+
return true;
681+
}
682+
return false;
683+
} catch {
684+
//empty
685+
}
686+
const nextRoot = path.dirname(dir);
687+
if (nextRoot === dir) {
688+
break;
689+
}
690+
dir = nextRoot;
691+
}
692+
return false;
693+
}
694+
695+
return {
696+
name: 'checkQwikExternals',
697+
enforce: 'pre',
698+
configResolved: (config) => {
699+
rootDir = config.root;
700+
},
701+
resolveId: {
702+
order: 'pre',
703+
async handler(source, importer, options) {
704+
if (source.startsWith('node:') || seen.has(source)) {
705+
return;
706+
}
707+
// technically we should check for each importer, but this is ok
708+
seen.add(source);
709+
const result = await this.resolve(source, importer, { ...options, skipSelf: true });
710+
if (result?.external) {
711+
// For Qwik files, check if they should be externalized
712+
if (await isQwikDep(source, importer ? path.dirname(importer) : rootDir)) {
713+
// TODO link to docs
714+
throw new Error(
715+
`\n==============\n${source} is being treated as an external dependency, but it should be included in the server bundle, because it uses Qwik.\nPlease add the package to "ssr.noExternal" in the Vite config. \n==============`
716+
);
717+
}
718+
}
719+
},
720+
},
721+
} as const satisfies VitePlugin<never>;
657722
}
658723

659724
const ANSI_COLOR = {
@@ -715,90 +780,6 @@ export async function render(document, rootNode, opts) {
715780
}`;
716781
}
717782

718-
async function findDepPkgJsonPath(sys: OptimizerSystem, dep: string, parent: string) {
719-
const fs: typeof import('fs') = await sys.dynamicImport('node:fs');
720-
let root = parent;
721-
while (root) {
722-
const pkg = sys.path.join(root, 'node_modules', dep, 'package.json');
723-
try {
724-
await fs.promises.access(pkg);
725-
// use 'node:fs' version to match 'vite:resolve' and avoid realpath.native quirk
726-
// https://github.com/sveltejs/vite-plugin-svelte/issues/525#issuecomment-1355551264
727-
return fs.promises.realpath(pkg);
728-
} catch {
729-
//empty
730-
}
731-
const nextRoot = sys.path.dirname(root);
732-
if (nextRoot === root) {
733-
break;
734-
}
735-
root = nextRoot;
736-
}
737-
return undefined;
738-
}
739-
740-
const findQwikRoots = async (
741-
sys: OptimizerSystem,
742-
packageJsonDir: string
743-
): Promise<QwikPackages[]> => {
744-
const paths = new Map<string, string>();
745-
if (sys.env === 'node' || sys.env === 'bun') {
746-
const fs: typeof import('fs') = await sys.dynamicImport('node:fs');
747-
let prevPackageJsonDir: string | undefined;
748-
do {
749-
try {
750-
const data = await fs.promises.readFile(sys.path.join(packageJsonDir, 'package.json'), {
751-
encoding: 'utf-8',
752-
});
753-
754-
try {
755-
const packageJson = JSON.parse(data);
756-
const dependencies = packageJson['dependencies'];
757-
const devDependencies = packageJson['devDependencies'];
758-
759-
const packages: string[] = [];
760-
if (typeof dependencies === 'object') {
761-
packages.push(...Object.keys(dependencies));
762-
}
763-
if (typeof devDependencies === 'object') {
764-
packages.push(...Object.keys(devDependencies));
765-
}
766-
767-
const basedir = sys.cwd();
768-
await Promise.all(
769-
packages.map(async (id) => {
770-
const pkgJsonPath = await findDepPkgJsonPath(sys, id, basedir);
771-
if (pkgJsonPath) {
772-
const pkgJsonContent = await fs.promises.readFile(pkgJsonPath, 'utf-8');
773-
const pkgJson = JSON.parse(pkgJsonContent);
774-
const qwikPath = pkgJson['qwik'];
775-
if (!qwikPath) {
776-
return;
777-
}
778-
// Support multiple paths
779-
const allPaths = Array.isArray(qwikPath) ? qwikPath : [qwikPath];
780-
for (const p of allPaths) {
781-
paths.set(
782-
await fs.promises.realpath(sys.path.resolve(sys.path.dirname(pkgJsonPath), p)),
783-
id
784-
);
785-
}
786-
}
787-
})
788-
);
789-
} catch (e) {
790-
console.error(e);
791-
}
792-
} catch {
793-
// ignore errors if package.json not found
794-
}
795-
prevPackageJsonDir = packageJsonDir;
796-
packageJsonDir = sys.path.dirname(packageJsonDir);
797-
} while (packageJsonDir !== prevPackageJsonDir);
798-
}
799-
return Array.from(paths).map(([path, id]) => ({ path, id }));
800-
};
801-
802783
export const isNotNullable = <T>(v: T): v is NonNullable<T> => {
803784
return v != null;
804785
};

packages/qwik/src/optimizer/src/plugins/vite.unit.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const noExternal = [
4242
'@qwik.dev/core/internal',
4343
'@qwik.dev/core/server',
4444
'@qwik.dev/core/build',
45-
'@qwik.dev/router',
4645
];
4746

4847
const excludeDeps = [
@@ -53,7 +52,6 @@ const excludeDeps = [
5352
'@qwik.dev/core/jsx-dev-runtime',
5453
'@qwik.dev/core/build',
5554
'@qwik-client-manifest',
56-
'@qwik.dev/router',
5755
'@builder.io/qwik',
5856
'@builder.io/qwik-city',
5957
];

starters/dev-server.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ const appNames = readdirSync(startersAppsDir).filter(
4343
(p) => statSync(join(startersAppsDir, p)).isDirectory() && p !== "base",
4444
);
4545

46-
const rootDir = resolve(__dirname, "..");
47-
const packagesDir = resolve(rootDir, "packages");
48-
const qwikRouterMjs = join(packagesDir, "qwik-router", "lib", "index.qwik.mjs");
49-
5046
/** Used when qwik-router server is enabled */
5147
const qwikRouterVirtualEntry = "@router-ssr-entry";
5248
const entrySsrFileName = "entry.ssr.tsx";
@@ -198,16 +194,7 @@ export {
198194
plugins: [
199195
...plugins,
200196
optimizer.qwikVite({
201-
/**
202-
* normally qwik finds qwik-router via package.json but we don't want that
203-
* because it causes it to try to lookup the special qwik router imports
204-
* even when we're not actually importing qwik-router
205-
*/
206-
disableVendorScan: true,
207-
vendorRoots: enableRouterServer ? [qwikRouterMjs] : [],
208-
entryStrategy: {
209-
type: "segment",
210-
},
197+
entryStrategy: { type: "segment" },
211198
client: {
212199
manifestOutput(manifest) {
213200
clientManifest = manifest;
@@ -271,7 +258,8 @@ async function routerApp(
271258
appDir: string,
272259
) {
273260
const ssrPath = join(appDir, "server", `${qwikRouterVirtualEntry}.js`);
274-
261+
// it's ok in the devserver to import core multiple times
262+
(globalThis as any).__qwik = null;
275263
const mod = await import(file(ssrPath));
276264
const router: any = mod.router;
277265
router(req, res, () => {
@@ -289,6 +277,8 @@ async function ssrApp(
289277
manifest: QwikManifest,
290278
) {
291279
const ssrPath = join(appDir, "server", "entry.ssr.js");
280+
// it's ok in the devserver to import core multiple times
281+
(globalThis as any).__qwik = null;
292282
const mod = await import(file(ssrPath));
293283
const render: Render = mod.default ?? mod.render;
294284

starters/e2e/e2e.use-id.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect, Locator } from "@playwright/test";
1+
import { test, expect } from "@playwright/test";
22

33
test.describe("use-id", () => {
44
test.beforeEach(async ({ page }) => {

0 commit comments

Comments
 (0)