Skip to content

Commit 5360522

Browse files
committed
feat: vite improvements
1 parent 7cfc037 commit 5360522

File tree

31 files changed

+1616
-382
lines changed

31 files changed

+1616
-382
lines changed

packages/vite/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,43 @@ Vite bundler integration for NativeScript apps. Provides a minimal setup for fas
1212
npm i @nativescript/vite -D
1313
```
1414

15+
## Quick start (`init`)
16+
17+
To bootstrap an existing NativeScript app for Vite, run from your app root:
18+
19+
```bash
20+
npx nativescript-vite init
21+
```
22+
23+
This will:
24+
25+
- Generate a `vite.config.ts` using the detected project flavor (Angular, Vue, React, Solid, TypeScript, or JavaScript) and the corresponding helper from `@nativescript/vite`.
26+
- Add (or update) the following npm scripts in your app `package.json`:
27+
- `dev:ios`
28+
- `dev:android`
29+
- `dev:server:ios`
30+
- `dev:server:android`
31+
- `ios`
32+
- `android`
33+
- Add the devDependencies `concurrently` and `wait-on`.
34+
- Add the dependency `@valor/nativescript-websockets`.
35+
- Append `.ns-vite-build` to `.gitignore` if it is not already present.
36+
37+
After running `init`, you now have two ways to work with Vite:
38+
39+
1. HMR workflow
40+
41+
```bash
42+
npm run dev:ios
43+
```
44+
45+
2. Standard dev workflow (non-HMR)
46+
47+
```bash
48+
ns debug ios --no-hmr
49+
ns debug android --no-hmr
50+
```
51+
1552
## Usage
1653

1754
1) Create `vite.config.ts`:

packages/vite/bin/cli.cjs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env node
2+
3+
// Small CommonJS launcher that forwards to the ESM library's init helper.
4+
// This allows `npx @nativescript/vite init` to work reliably across npm versions.
5+
6+
(async () => {
7+
try {
8+
const mod = await import('../dist/index.cjs').catch(async () => import('../index.js'));
9+
if (process.argv[2] === 'init') {
10+
if (typeof mod.runInit === 'function') {
11+
await mod.runInit();
12+
} else if (mod && mod.helpers && typeof mod.helpers.runInit === 'function') {
13+
await mod.helpers.runInit();
14+
} else if (mod && mod.default && typeof mod.default.runInit === 'function') {
15+
await mod.default.runInit();
16+
} else {
17+
const initMod = await import('../helpers/init.js');
18+
if (typeof initMod.runInit === 'function') {
19+
await initMod.runInit();
20+
} else {
21+
console.error('[@nativescript/vite] `runInit` not found in helpers.');
22+
process.exitCode = 1;
23+
}
24+
}
25+
} else {
26+
console.log('Usage: npx @nativescript/vite init');
27+
}
28+
} catch (err) {
29+
console.error('[@nativescript/vite] CLI failed:', err && err.message ? err.message : err);
30+
process.exitCode = 1;
31+
}
32+
})();

packages/vite/configuration/angular.ts

Lines changed: 63 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { mergeConfig, type UserConfig, type Plugin } from 'vite';
22
import path from 'path';
33
import { createRequire } from 'node:module';
44
import angular from '@analogjs/vite-plugin-angular';
5-
import { angularLinkerVitePlugin, angularLinkerVitePluginPost } from '../helpers/angular-linker.js';
5+
import { angularLinkerVitePlugin, angularLinkerVitePluginPost } from '../helpers/angular/angular-linker.js';
6+
import { ensureSharedAngularLinker } from '../helpers/angular/shared-linker.js';
7+
import { containsRealNgDeclare } from '../helpers/angular/util.js';
68
import { baseConfig } from './base.js';
9+
import { getCliFlags } from '../helpers/cli-flags.js';
710

811
// Rollup-level linker to guarantee Angular libraries are linked when included in the bundle graph.
912
function angularRollupLinker(projectRoot?: string): Plugin {
@@ -101,6 +104,10 @@ function angularRollupLinker(projectRoot?: string): Plugin {
101104
};
102105
}
103106

107+
const cliFlags = getCliFlags();
108+
const isDevEnv = process.env.NODE_ENV !== 'production';
109+
const hmrActive = isDevEnv && !!cliFlags.hmr;
110+
104111
const plugins = [
105112
// Allow external html template changes to trigger hot reload: Make .ts files depend on their .html templates
106113
{
@@ -151,6 +158,17 @@ const plugins = [
151158
},
152159
} as any;
153160
},
161+
configResolved(resolved) {
162+
const deps = (resolved.optimizeDeps ||= {} as any);
163+
deps.noDiscovery = true;
164+
deps.entries = [];
165+
deps.include = [];
166+
const exclude = new Set<string>(Array.isArray(deps.exclude) ? deps.exclude : []);
167+
['@nativescript/core', 'rxjs', '@valor/nativescript-websockets', 'set-value', 'react', 'react-reconciler', 'react-nativescript'].forEach((x) => exclude.add(x));
168+
deps.exclude = Array.from(exclude);
169+
const esbuildOptions = (deps.esbuildOptions ||= {});
170+
esbuildOptions.plugins = [];
171+
},
154172
},
155173
];
156174

@@ -166,86 +184,12 @@ export const angularConfig = ({ mode }): UserConfig => {
166184
apply: 'build' as const,
167185
enforce: 'post' as const,
168186
async generateBundle(_options, bundle) {
169-
function containsRealNgDeclare(src: string): boolean {
170-
// scan while skipping strings and comments; detect ɵɵngDeclare outside them
171-
let inStr = false,
172-
strCh = '',
173-
esc = false,
174-
inBlk = false,
175-
inLine = false;
176-
for (let i = 0; i < src.length; i++) {
177-
const ch = src[i];
178-
const next = src[i + 1];
179-
if (inLine) {
180-
if (ch === '\n') inLine = false;
181-
continue;
182-
}
183-
if (inBlk) {
184-
if (ch === '*' && next === '/') {
185-
inBlk = false;
186-
i++;
187-
}
188-
continue;
189-
}
190-
if (inStr) {
191-
if (esc) {
192-
esc = false;
193-
continue;
194-
}
195-
if (ch === '\\') {
196-
esc = true;
197-
continue;
198-
}
199-
if (ch === strCh) {
200-
inStr = false;
201-
strCh = '';
202-
}
203-
continue;
204-
}
205-
if (ch === '/' && next === '/') {
206-
inLine = true;
207-
i++;
208-
continue;
209-
}
210-
if (ch === '/' && next === '*') {
211-
inBlk = true;
212-
i++;
213-
continue;
214-
}
215-
if (ch === '"' || ch === "'" || ch === '`') {
216-
inStr = true;
217-
strCh = ch;
218-
continue;
219-
}
220-
// fast path for ɵ + call-like ngDeclare pattern (e.g., ɵɵngDeclareDirective(...))
221-
if (ch === 'ɵ' && next === 'ɵ' && src.startsWith('ɵɵngDeclare', i)) {
222-
let j = i + 'ɵɵngDeclare'.length;
223-
// consume identifier tail (e.g., ClassMetadata, Directive, Component, Factory, Injectable, etc.)
224-
while (j < src.length) {
225-
const cj = src.charCodeAt(j);
226-
// [A-Za-z0-9_$]
227-
if ((cj >= 65 && cj <= 90) || (cj >= 97 && cj <= 122) || (cj >= 48 && cj <= 57) || cj === 95 || cj === 36) j++;
228-
else break;
229-
}
230-
// skip whitespace
231-
while (j < src.length && /\s/.test(src[j])) j++;
232-
if (src[j] === '(') return true; // it's a call site
233-
}
234-
}
235-
return false;
236-
}
237-
// Lazy load linker deps to avoid hard coupling
238-
let babel: any = null;
239-
let createLinker: any = null;
240-
try {
241-
babel = await import('@babel/core');
242-
const linkerMod: any = await import('@angular/compiler-cli/linker/babel');
243-
createLinker = linkerMod.createLinkerPlugin || linkerMod.createEs2015LinkerPlugin;
244-
} catch {
245-
return; // no linker available
187+
function isNsAngularPolyfillsChunk(chunk: any): boolean {
188+
if (!chunk || !(chunk as any).modules) return false;
189+
return Object.keys((chunk as any).modules).some((m) => m.includes('node_modules/@nativescript/angular/fesm2022/nativescript-angular-polyfills.mjs'));
246190
}
247-
if (!babel || !createLinker) return;
248-
const plugin = createLinker({ sourceMapping: false });
191+
const { babel, linkerPlugin } = await ensureSharedAngularLinker(process.cwd());
192+
if (!babel || !linkerPlugin) return;
249193
const strict = process.env.NS_STRICT_NG_LINK === '1' || process.env.NS_STRICT_NG_LINK === 'true';
250194
const enforceStrict = process.env.NS_STRICT_NG_LINK_ENFORCE === '1' || process.env.NS_STRICT_NG_LINK_ENFORCE === 'true';
251195
const debug = process.env.VITE_DEBUG_LOGS === '1' || process.env.VITE_DEBUG_LOGS === 'true';
@@ -255,36 +199,34 @@ export const angularConfig = ({ mode }): UserConfig => {
255199
if (chunk && (chunk as any).type === 'chunk') {
256200
const code = (chunk as any).code as string;
257201
if (!code) continue;
202+
const isNsPolyfills = isNsAngularPolyfillsChunk(chunk);
258203
try {
259204
const res = await (babel as any).transformAsync(code, {
260205
filename: fileName,
261206
configFile: false,
262207
babelrc: false,
263208
sourceMaps: false,
264209
compact: false,
265-
plugins: [plugin],
210+
plugins: [linkerPlugin],
266211
});
267-
if (res?.code && res.code !== code) {
268-
(chunk as any).code = res.code;
212+
const finalCode = res?.code && res.code !== code ? res.code : code;
213+
if (finalCode !== code) {
214+
(chunk as any).code = finalCode;
269215
if (debug) {
270216
try {
271-
console.log('[ns-angular-linker][post] linked', fileName);
217+
console.log('[ns-angular-linker][post] linked', fileName, isNsPolyfills ? '(polyfills)' : '');
272218
} catch {}
273219
}
274-
} else if (strict) {
275-
// Only flag if we still detect actual Ivy partial declarations (ɵɵngDeclare*) outside strings/comments
276-
if (containsRealNgDeclare(code)) {
277-
unlinked.push(fileName);
278-
}
220+
}
221+
if (strict && !isNsPolyfills && containsRealNgDeclare(finalCode)) {
222+
unlinked.push(fileName);
279223
}
280224
} catch {
281-
// best effort; keep original code
282225
if (strict) unlinked.push(fileName);
283226
}
284227
}
285228
}
286229
if (strict && unlinked.length) {
287-
// Provide extra diagnostics: list a few module ids from bundle to locate offending sources
288230
const details: string[] = [];
289231
for (const fname of unlinked) {
290232
const chunk: any = (bundle as any)[fname];
@@ -293,7 +235,8 @@ export const angularConfig = ({ mode }): UserConfig => {
293235
.filter((m) => /node_modules\/(?:@angular|@nativescript\/angular)\//.test(m))
294236
.slice(0, 8)
295237
: [];
296-
// Add a small code excerpt around the first occurrence for easier inspection
238+
const isOnlyPolyfills = modIds.length > 0 && modIds.every((m) => m.includes('node_modules/@nativescript/angular/fesm2022/nativescript-angular-polyfills.mjs'));
239+
if (isOnlyPolyfills) continue;
297240
let snippet = '';
298241
try {
299242
const code = (chunk as any).code as string;
@@ -306,6 +249,7 @@ export const angularConfig = ({ mode }): UserConfig => {
306249
} catch {}
307250
details.push(` - ${fname}${modIds.length ? `\n from: ${modIds.join('\n ')}` : ''}${snippet}`);
308251
}
252+
if (!details.length) return;
309253
const message = `Angular linker strict mode: found unlinked partial declarations in emitted chunks: \n` + details.join('\n') + `\nSet NS_STRICT_NG_LINK=0 to disable this check. Set NS_STRICT_NG_LINK_ENFORCE=1 to make this a hard error.`;
310254
if (enforceStrict) {
311255
throw new Error(message);
@@ -326,93 +270,40 @@ export const angularConfig = ({ mode }): UserConfig => {
326270
async renderChunk(code: string, chunk: any) {
327271
try {
328272
if (!code) return null;
329-
// quick guard: look for real ɵɵngDeclare call outside strings/comments
330-
const hasReal = (() => {
331-
let inStr = false,
332-
strCh = '',
333-
esc = false,
334-
inBlk = false,
335-
inLine = false;
336-
for (let i = 0; i < code.length; i++) {
337-
const ch = code[i],
338-
n = code[i + 1];
339-
if (inLine) {
340-
if (ch === '\n') inLine = false;
341-
continue;
342-
}
343-
if (inBlk) {
344-
if (ch === '*' && n === '/') {
345-
inBlk = false;
346-
i++;
347-
}
348-
continue;
349-
}
350-
if (inStr) {
351-
if (esc) {
352-
esc = false;
353-
continue;
354-
}
355-
if (ch === '\\') {
356-
esc = true;
357-
continue;
358-
}
359-
if (ch === strCh) {
360-
inStr = false;
361-
strCh = '';
362-
}
363-
continue;
364-
}
365-
if (ch === '/' && n === '/') {
366-
inLine = true;
367-
i++;
368-
continue;
369-
}
370-
if (ch === '/' && n === '*') {
371-
inBlk = true;
372-
i++;
373-
continue;
374-
}
375-
if (ch === '"' || ch === "'" || ch === '`') {
376-
inStr = true;
377-
strCh = ch;
378-
continue;
379-
}
380-
if (ch === 'ɵ' && n === 'ɵ' && code.startsWith('ɵɵngDeclare', i)) {
381-
let j = i + 'ɵɵngDeclare'.length;
382-
while (j < code.length) {
383-
const cj = code.charCodeAt(j);
384-
if ((cj >= 65 && cj <= 90) || (cj >= 97 && cj <= 122) || (cj >= 48 && cj <= 57) || cj === 95 || cj === 36) j++;
385-
else break;
386-
}
387-
while (j < code.length && /\s/.test(code[j])) j++;
388-
if (code[j] === '(') return true;
389-
}
273+
if (!containsRealNgDeclare(code)) return null;
274+
const { babel, linkerPlugin } = await ensureSharedAngularLinker(process.cwd());
275+
if (!babel || !linkerPlugin) return null;
276+
const filename = chunk.fileName || chunk.name || 'chunk.mjs';
277+
const debug = process.env.VITE_DEBUG_LOGS === '1' || process.env.VITE_DEBUG_LOGS === 'true';
278+
const runLink = async (input: string) => {
279+
const result = await (babel as any).transformAsync(input, {
280+
filename,
281+
configFile: false,
282+
babelrc: false,
283+
sourceMaps: false,
284+
compact: false,
285+
plugins: [linkerPlugin],
286+
});
287+
return result?.code ?? input;
288+
};
289+
let transformed = await runLink(code);
290+
if (containsRealNgDeclare(transformed)) {
291+
transformed = await runLink(transformed);
292+
}
293+
if (transformed !== code) {
294+
if (debug) {
295+
try {
296+
console.log('[ns-angular-linker][render] linked', filename);
297+
} catch {}
390298
}
391-
return false;
392-
})();
393-
if (!hasReal) return null;
394-
const babel: any = await import('@babel/core');
395-
const linkerMod: any = await import('@angular/compiler-cli/linker/babel');
396-
const createLinker = linkerMod.createLinkerPlugin || linkerMod.createEs2015LinkerPlugin;
397-
if (!babel || !createLinker) return null;
398-
const plugin = createLinker({ sourceMapping: false });
399-
const result = await (babel as any).transformAsync(code, {
400-
filename: chunk.fileName || chunk.name || 'chunk.mjs',
401-
configFile: false,
402-
babelrc: false,
403-
sourceMaps: false,
404-
compact: false,
405-
plugins: [plugin],
406-
});
407-
if (result?.code && result.code !== code) {
408-
return { code: result.code, map: null };
299+
return { code: transformed, map: null };
409300
}
410301
} catch {}
411302
return null;
412303
},
413304
};
414305

415-
const enableRollupLinker = process.env.NS_ENABLE_ROLLUP_LINKER === '1' || process.env.NS_ENABLE_ROLLUP_LINKER === 'true';
306+
const enableRollupLinker = process.env.NS_ENABLE_ROLLUP_LINKER === '1' || process.env.NS_ENABLE_ROLLUP_LINKER === 'true' || hmrActive;
416307

417308
return mergeConfig(baseConfig({ mode }), {
418309
plugins: [...plugins, ...(enableRollupLinker ? [angularRollupLinker(process.cwd())] : []), renderChunkLinker, postLinker],

0 commit comments

Comments
 (0)