Skip to content

Commit 63ed104

Browse files
authored
feat(vite): framework registration API (#11358)
[skip ci]
1 parent 43f5538 commit 63ed104

28 files changed

Lines changed: 1002 additions & 36 deletions

packages/vite/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ npx nativescript-vite init
4444

4545
This will:
4646

47-
- Generate a `vite.config.mts` using the detected project flavor (Angular, Vue, React, Solid, TypeScript, or JavaScript) and the corresponding helper subpath from `@nativescript/vite`.
47+
- Generate a `vite.config.mts` using the detected project flavor (Angular, Vue, React, Solid, TypeScript, or JavaScript — or a flavor a dependency declares, see below) and the corresponding helper subpath from `@nativescript/vite`.
4848
- Add the dependency `@valor/nativescript-websockets`.
4949
- Append `.ns-vite-build` to `.gitignore` if it is not already present.
5050

@@ -152,6 +152,22 @@ import { solidConfig } from '@nativescript/vite/solid';
152152
import { vueConfig } from '@nativescript/vite/vue';
153153
```
154154

155+
### Flavors from other packages
156+
157+
A framework can ship its own flavor — config helper, server strategy and device-side
158+
client strategy — as a package, using `@nativescript/vite/framework` and
159+
`@nativescript/vite/hmr/client/framework.js`. `init` and flavor detection pick it up from a
160+
`nativescript.vite` declaration in that package's `package.json`. The Octane flavor,
161+
[`@nativescript-community/vite-octane`](https://github.com/nativescript-community/octane), is built this way:
162+
163+
```ts
164+
import { octaneConfig } from '@nativescript-community/vite-octane';
165+
166+
export default defineConfig(({ mode }) => octaneConfig({ mode }));
167+
```
168+
169+
See [docs/framework-flavors.md](./docs/framework-flavors.md) for the full walkthrough.
170+
155171
2) Update `nativescript.config.ts`:
156172

157173
```ts

packages/vite/configuration/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export const baseConfig = ({ mode, flavor }: { mode: string; flavor?: string }):
120120
}
121121

122122
// Filtered logger to suppress noisy warnings
123-
const filteredLogger = createFilteredViteLogger();
123+
const filteredLogger = createFilteredViteLogger({ hmrActive });
124124

125125
// Create TypeScript aliases with platform support
126126
const tsConfig = getTsConfigData({ platform, verbose });

packages/vite/docs/framework-flavors.md

Lines changed: 241 additions & 0 deletions
Large diffs are not rendered by default.

packages/vite/framework.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* `@nativescript/vite/framework` — the surface a framework package uses to
3+
* ship its own NativeScript HMR flavor (dev-server side, Node).
4+
*
5+
* A flavor is a name, a server strategy, and a client strategy module. The
6+
* server strategy runs in the Vite process; the client strategy is fetched by
7+
* the device next to the shared HMR client and is authored against
8+
* `@nativescript/vite/hmr/client/framework.js`.
9+
*/
10+
export { registerFrameworkFlavor, getFrameworkFlavor, getClientStrategyDevicePath, isBuiltInFlavor } from './hmr/framework-flavors.js';
11+
export type { FrameworkFlavorDefinition } from './hmr/framework-flavors.js';
12+
13+
export type { FrameworkServerStrategy, FrameworkProcessFileContext, FrameworkRegistryContext, FrameworkServedModuleContext, FrameworkModuleRequestContext, FrameworkRouteContext } from './hmr/server/framework-strategy.js';
14+
export type { FrameworkClientStrategy, FrameworkClientBatchContext, FrameworkClientMessageContext, FrameworkClientMountContext, ClientGraphModule } from './hmr/client/framework-client-strategy.js';
15+
16+
/** The generic device-module pipeline; the usual base for a new server strategy. */
17+
export { typescriptServerStrategy } from './hmr/frameworks/typescript/server/strategy.js';
18+
/** Shared hot-update prologue every server strategy's `handleHotUpdate` starts with. */
19+
export { runHotUpdatePrologue } from './hmr/server/websocket-hot-update.js';
20+
export type { NsHotUpdateContext, HotUpdatePrologueState, HmrUpdateMetrics } from './hmr/server/websocket-hot-update.js';
21+
export { purgeTransformCachesForHotUpdate } from './hmr/server/transform-cache-invalidation.js';
22+
23+
export { baseConfig } from './configuration/base.js';
24+
export { getTypeCheckPlugins } from './helpers/typescript-check.js';
25+
export type { TypeCheckControlOptions, TypeCheckSetting, TypeCheckFlavor } from './helpers/typescript-check.js';

packages/vite/helpers/flavor.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
1-
// import { defaultConfigs } from '..';
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import * as path from 'node:path';
23
import { getAllDependencies } from './utils.js';
4+
import { findMonorepoWorkspaceRoot, getProjectRootPath } from './project.js';
5+
6+
/**
7+
* A flavor declared by a framework package in its own package.json:
8+
*
9+
* "nativescript": { "vite": { "flavor": "octane", "config": { "import": "octaneConfig", "from": "@nativescript-community/vite-octane" } } }
10+
*
11+
* The dependency that carries it identifies the flavor for detection, and
12+
* `config` tells `nativescript-vite init` which helper to scaffold.
13+
*/
14+
export interface DeclaredViteFlavor {
15+
flavor: string;
16+
package: string;
17+
config?: { import: string; from: string };
18+
}
19+
20+
function readDeclaredViteFlavor(dependency: string): DeclaredViteFlavor | null {
21+
const projectRoot = getProjectRootPath();
22+
const roots = [projectRoot, findMonorepoWorkspaceRoot(projectRoot)].filter((root): root is string => !!root);
23+
for (const root of roots) {
24+
const manifest = path.join(root, 'node_modules', dependency, 'package.json');
25+
if (!existsSync(manifest)) continue;
26+
try {
27+
const vite = JSON.parse(readFileSync(manifest, 'utf8'))?.nativescript?.vite;
28+
if (vite && typeof vite.flavor === 'string' && vite.flavor) {
29+
const config = vite.config && typeof vite.config.import === 'string' && typeof vite.config.from === 'string' ? { import: vite.config.import, from: vite.config.from } : undefined;
30+
return { flavor: vite.flavor, package: dependency, config };
31+
}
32+
} catch {}
33+
return null;
34+
}
35+
return null;
36+
}
37+
38+
/** The first installed dependency that declares a Vite flavor, if any. */
39+
export function findDeclaredViteFlavor(): DeclaredViteFlavor | null {
40+
for (const dependency of getAllDependencies()) {
41+
const declared = readDeclaredViteFlavor(dependency);
42+
if (declared) return declared;
43+
}
44+
return null;
45+
}
346

447
let targetFlavor: string;
548

@@ -59,6 +102,11 @@ export function determineProjectFlavor(): string | false {
59102
return 'svelte';
60103
}
61104

105+
const declared = findDeclaredViteFlavor();
106+
if (declared) {
107+
return declared.flavor;
108+
}
109+
62110
// the order is important - angular, react, and svelte also include these deps
63111
// but should return prior to this condition!
64112
if (dependencies.includes('@nativescript/core') && dependencies.includes('typescript')) {

packages/vite/helpers/global-defines.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getProjectAppPath, getProjectAppVirtualPath } from './utils.js';
2+
import { getClientStrategyDevicePath } from '../hmr/framework-flavors.js';
23

34
const APP_ROOT_DIR = getProjectAppPath();
45
const APP_ROOT_VIRTUAL = getProjectAppVirtualPath();
@@ -93,6 +94,9 @@ export function getRuntimeSeedValues(opts: { platform?: string; isDevMode: boole
9394
isIOS: values.__APPLE__,
9495
// Runtime flavor for the raw-served HMR client's TARGET_FLAVOR resolution.
9596
__NS_TARGET_FLAVOR__: opts.flavor,
97+
// Device path of a registered (non built-in) flavor's client strategy;
98+
// '' for built-ins, which the client resolves from its own package.
99+
__NS_CLIENT_STRATEGY_URL__: getClientStrategyDevicePath(opts.flavor),
96100
// App-root virtual path — every served-id → moduleName mapping (frame
97101
// navigation targets, modal re-present matching) depends on this.
98102
__NS_APP_ROOT_DIR__: APP_ROOT_DIR,
@@ -247,6 +251,7 @@ export function getGlobalDefines(opts: { platform: string; targetMode: string; v
247251
__non_webpack_require__: 'globalThis.require',
248252
__NS_ENV_VERBOSE__: JSON.stringify(values.__NS_ENV_VERBOSE__),
249253
__NS_TARGET_FLAVOR__: JSON.stringify(opts.flavor),
254+
__NS_CLIENT_STRATEGY_URL__: JSON.stringify(getClientStrategyDevicePath(opts.flavor)),
250255
// whether to show the HMR in-progress overlay.
251256
__NS_HMR_PROGRESS_OVERLAY_ENABLED__: JSON.stringify(isHmrProgressOverlayEnabled()),
252257
__CSS_PARSER__: JSON.stringify(values.__CSS_PARSER__),

packages/vite/helpers/init.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { createRequire } from 'node:module';
4-
import { determineProjectFlavor } from './flavor.js';
4+
import { determineProjectFlavor, findDeclaredViteFlavor } from './flavor.js';
55
import { getProjectFilePath, getProjectRootPath } from './project.js';
66

77
const require = createRequire(import.meta.url);
@@ -90,14 +90,27 @@ function getFlavorImportAndConfig(flavor: string): { importLine: string; configE
9090
configExpr: 'typescriptConfig({ mode })',
9191
};
9292
case 'javascript':
93-
default:
94-
return {
95-
importLine: "import { javascriptConfig } from '@nativescript/vite/javascript';",
96-
configExpr: 'javascriptConfig({ mode })',
97-
};
93+
return javascriptImportAndConfig();
94+
default: {
95+
const declared = findDeclaredViteFlavor();
96+
if (declared?.flavor === flavor && declared.config) {
97+
return {
98+
importLine: `import { ${declared.config.import} } from '${declared.config.from}';`,
99+
configExpr: `${declared.config.import}({ mode })`,
100+
};
101+
}
102+
return javascriptImportAndConfig();
103+
}
98104
}
99105
}
100106

107+
function javascriptImportAndConfig(): { importLine: string; configExpr: string } {
108+
return {
109+
importLine: "import { javascriptConfig } from '@nativescript/vite/javascript';",
110+
configExpr: 'javascriptConfig({ mode })',
111+
};
112+
}
113+
101114
function ensureViteConfig() {
102115
const root = getProjectRootPath();
103116
const existing = ['vite.config.mts', 'vite.config.ts', 'vite.config.mjs', 'vite.config.js', 'vite.config.cts', 'vite.config.cjs'].find((name) => fs.existsSync(path.join(root, name)));

packages/vite/helpers/logging.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,13 @@ describe('shouldSuppressViteWarning', () => {
162162
});
163163
});
164164
});
165+
166+
describe('shouldSuppressViteInfo', () => {
167+
it('drops the stock web-client HMR verdicts, which never apply to a device session', async () => {
168+
const { shouldSuppressViteInfo } = await import('./logging.js');
169+
expect(shouldSuppressViteInfo('page reload src/octane/driver.ts')).toBe(true);
170+
expect(shouldSuppressViteInfo('hmr update /src/app.tsx')).toBe(true);
171+
expect(shouldSuppressViteInfo(' VITE v8.2.2 ready in 1007 ms')).toBe(false);
172+
expect(shouldSuppressViteInfo('server restarted.')).toBe(false);
173+
});
174+
});

packages/vite/helpers/logging.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,14 @@ export function clearVerboseCache(): void {
104104
// All matching uses `.includes()` (never `.startsWith()`) because Vite wraps
105105
// some warnings in picocolors ANSI escape sequences before handing them to
106106
// the logger, which would defeat `startsWith`-style probes on TTY output.
107-
export function createFilteredViteLogger(): Logger {
107+
export function createFilteredViteLogger(options: { hmrActive?: boolean } = {}): Logger {
108108
const baseLogger = createLogger(undefined, { allowClearScreen: true });
109109
return {
110110
...baseLogger,
111+
info(message: any, opts?: any) {
112+
if (options.hmrActive && shouldSuppressViteInfo(String(message || ''))) return;
113+
return baseLogger.info(message, opts);
114+
},
111115
warn(message: any, options?: any) {
112116
const msg = String(message || '');
113117
if (shouldSuppressViteWarning(msg)) return;
@@ -121,6 +125,17 @@ export function createFilteredViteLogger(): Logger {
121125
};
122126
}
123127

128+
/**
129+
* Vite's stock HMR client never connects under device HMR — the device talks
130+
* to `/ns-hmr` — so Vite's own verdicts about that client are noise, and one
131+
* of them misleads: `page reload <file>` is what Vite decides for any module
132+
* it cannot hot-accept on the web, printed while the device is applying the
133+
* same save in place through a framework strategy.
134+
*/
135+
export function shouldSuppressViteInfo(msg: string): boolean {
136+
return /\bpage reload\b/.test(msg) || /\bhmr update\b/.test(msg);
137+
}
138+
124139
// Exported for unit tests. Keep this function pure so the test suite can
125140
// exercise every suppression pattern without instantiating a real logger.
126141
export function shouldSuppressViteWarning(msg: string): boolean {

packages/vite/helpers/typescript-check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Platform } from './platform-types.js';
1010
const require = createRequire(import.meta.url);
1111

1212
export type PlatformType = Platform;
13-
type TypeCheckFlavor = 'typescript' | 'react' | 'solid' | 'vue' | 'angular' | 'javascript';
13+
export type TypeCheckFlavor = 'typescript' | 'react' | 'solid' | 'vue' | 'angular' | 'javascript';
1414

1515
export type TypeCheckMode = 'off' | 'warn' | 'error';
1616

0 commit comments

Comments
 (0)