-
-
Notifications
You must be signed in to change notification settings - Fork 120
fix build --watch
#1189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix build --watch
#1189
Changes from 1 commit
a2e8363
e00ba38
ea2e4dd
efa1b4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,15 @@ | ||
import path from 'node:path'; | ||
import fs from 'node:fs'; | ||
import MagicString from 'magic-string'; | ||
|
||
function replaceWithSourceMap(code, value, replacement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the missing sourcemap lead to error logs, add them to allow easier debugging and assertions that no unexpected errors happened |
||
const s = new MagicString(code); | ||
s.replaceAll(value, replacement); | ||
return { | ||
code: s.toString(), | ||
map: s.generateMap({ hires: 'boundary' }) | ||
}; | ||
} | ||
/** | ||
* Ensure transform flow is not interrupted | ||
* @returns {import('vite').Plugin[]} | ||
|
@@ -11,19 +21,19 @@ export function transformValidation() { | |
enforce: 'pre', | ||
transform(code, id) { | ||
if (id.endsWith('.svelte')) { | ||
return code.replaceAll('__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__'); | ||
return replaceWithSourceMap(code, '__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__'); | ||
} else if (id.endsWith('.css')) { | ||
return code.replaceAll('__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__'); | ||
return replaceWithSourceMap(code, '__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__'); | ||
} | ||
} | ||
}, | ||
{ | ||
name: 'transform-validation:2', | ||
transform(code, id) { | ||
if (id.endsWith('.svelte')) { | ||
return code.replaceAll('__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__'); | ||
return replaceWithSourceMap(code, '__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__'); | ||
} else if (id.endsWith('.css')) { | ||
return code.replaceAll('__CSS_TRANSFORM_2__', 'red'); | ||
return replaceWithSourceMap(code, '__CSS_TRANSFORM_2__', 'red'); | ||
} | ||
} | ||
}, | ||
|
@@ -32,7 +42,7 @@ export function transformValidation() { | |
enforce: 'post', | ||
transform(code, id) { | ||
if (id.endsWith('.svelte')) { | ||
return code.replaceAll('__JS_TRANSFORM_3__', 'Hello world'); | ||
replaceWithSourceMap(code, '__JS_TRANSFORM_3__', 'Hello world'); | ||
} | ||
// can't handle css here as in build, it would be `export default {}` | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,5 +6,8 @@ | |
"main": "./index.js", | ||
"files": [ | ||
"index.js" | ||
] | ||
], | ||
"dependencies": { | ||
"magic-string": "^0.30.17" | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is basically a copy of the hmr test, with some changed asserts (state cannot be preserved, manual page reloads more often) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { | ||
isBuildWatch, | ||
getEl, | ||
getText, | ||
editFileAndWaitForBuildWatchComplete, | ||
hmrCount, | ||
untilMatches, | ||
sleep, | ||
getColor, | ||
editFile, | ||
addFile, | ||
removeFile, | ||
editViteConfig, | ||
browserLogs, | ||
waitForBuildWatchAndPageReload | ||
} from '~utils'; | ||
|
||
test('should render App', async () => { | ||
expect(await getText('#app-header')).toBe('Test-App'); | ||
}); | ||
|
||
test('should render static import', async () => { | ||
expect(await getText('#static-import .label')).toBe('static-import'); | ||
}); | ||
|
||
test('should render dependency import', async () => { | ||
expect(await getText('#dependency-import .label')).toBe('dependency-import'); | ||
}); | ||
|
||
test('should render dynamic import', async () => { | ||
expect(await getEl('#dynamic-import')).toBe(null); | ||
const dynamicImportButton = await getEl('#button-import-dynamic'); | ||
expect(dynamicImportButton).toBeDefined(); | ||
await dynamicImportButton.click(); | ||
await untilMatches( | ||
() => getText('#dynamic-import .label'), | ||
'dynamic-import', | ||
'dynamic import loaded after click' | ||
); | ||
}); | ||
|
||
test('should not have failed requests', async () => { | ||
browserLogs.forEach((msg) => { | ||
expect(msg).not.toMatch('404'); | ||
}); | ||
}); | ||
|
||
test('should respect transforms', async () => { | ||
expect(await getText('#js-transform')).toBe('Hello world'); | ||
expect(await getColor('#css-transform')).toBe('red'); | ||
}); | ||
|
||
if (isBuildWatch) { | ||
describe('build --watch', () => { | ||
const updateHmrTest = editFileAndWaitForBuildWatchComplete.bind( | ||
null, | ||
'src/components/HmrTest.svelte' | ||
); | ||
const updateModuleContext = editFileAndWaitForBuildWatchComplete.bind( | ||
null, | ||
'src/components/partial-hmr/ModuleContext.svelte' | ||
); | ||
const updateApp = editFileAndWaitForBuildWatchComplete.bind(null, 'src/App.svelte'); | ||
const updateStore = editFileAndWaitForBuildWatchComplete.bind(null, 'src/stores/hmr-stores.js'); | ||
|
||
test('should have expected initial state', async () => { | ||
// initial state, both counters 0, both labels red | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
expect(await getText('#hmr-test-2 .counter')).toBe('0'); | ||
expect(await getText('#hmr-test-1 .label')).toBe('hmr-test'); | ||
expect(await getText('#hmr-test-2 .label')).toBe('hmr-test'); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
expect(await getColor('#hmr-test-2 .label')).toBe('red'); | ||
}); | ||
|
||
test('should have working increment button', async () => { | ||
// increment counter of one instance to have local state to verify after build updates | ||
await (await getEl('#hmr-test-1 .increment')).click(); | ||
await sleep(50); | ||
|
||
// counter1 = 1, counter2 = 0 | ||
expect(await getText('#hmr-test-1 .counter')).toBe('1'); | ||
expect(await getText('#hmr-test-2 .counter')).toBe('0'); | ||
}); | ||
|
||
test('should apply css changes in HmrTest.svelte', async () => { | ||
// update style, change label color from red to green | ||
await updateHmrTest((content) => content.replace('color: red', 'color: green')); | ||
|
||
// color should have changed | ||
expect(await getColor('#hmr-test-1 .label')).toBe('green'); | ||
expect(await getColor('#hmr-test-2 .label')).toBe('green'); | ||
}); | ||
|
||
test('should apply js change in HmrTest.svelte ', async () => { | ||
// update script, change label value | ||
await updateHmrTest((content) => | ||
content.replace("const label = 'hmr-test'", "const label = 'hmr-test-updated'") | ||
); | ||
expect(await getText('#hmr-test-1 .label')).toBe('hmr-test-updated'); | ||
expect(await getText('#hmr-test-2 .label')).toBe('hmr-test-updated'); | ||
}); | ||
|
||
test('should reset state of external store used by HmrTest.svelte when editing App.svelte', async () => { | ||
// update App, add a new instance of HmrTest | ||
await updateApp((content) => | ||
content.replace( | ||
'<!-- HMR-TEMPLATE-INJECT -->', | ||
'<HmrTest id="hmr-test-3"/>\n<!-- HMR-TEMPLATE-INJECT -->' | ||
) | ||
); | ||
// counter state is reset | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
expect(await getText('#hmr-test-2 .counter')).toBe('0'); | ||
// a third instance has been added | ||
expect(await getText('#hmr-test-3 .counter')).toBe('0'); | ||
}); | ||
|
||
test('should reset state of store when editing hmr-stores.js', async () => { | ||
// change state | ||
await (await getEl('#hmr-test-2 .increment')).click(); | ||
await sleep(50); | ||
expect(await getText('#hmr-test-2 .counter')).toBe('1'); | ||
await updateStore((content) => `${content}\n/*trigger change*/\n`); | ||
// counter state is reset | ||
expect(await getText('#hmr-test-2 .counter')).toBe('0'); | ||
}); | ||
|
||
test('should work when editing script context="module"', async () => { | ||
expect(await getText('#hmr-with-context')).toContain('x=0 y=1 slot=1'); | ||
expect(await getText('#hmr-without-context')).toContain('x=0 y=1 slot='); | ||
expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(0); | ||
expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0); | ||
await updateModuleContext((content) => content.replace('y = 1', 'y = 2')); | ||
expect(await getText('#hmr-with-context')).toContain('x=0 y=2 slot=2'); | ||
expect(await getText('#hmr-without-context')).toContain('x=0 y=2 slot='); | ||
expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(1); | ||
expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0); | ||
}); | ||
|
||
test('should work with emitCss: false in vite config', async () => { | ||
await editViteConfig((c) => c.replace('svelte()', 'svelte({emitCss:false})')); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('green'); | ||
await (await getEl('#hmr-test-1 .increment')).click(); | ||
await sleep(50); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('1'); | ||
await updateHmrTest((content) => content.replace('color: green', 'color: red')); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
// counter value is reset | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
}); | ||
|
||
test('should work with emitCss: false in svelte config', async () => { | ||
addFile('svelte.config.js', 'export default {vitePlugin:{emitCss:false}}'); | ||
await waitForBuildWatchAndPageReload(); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
removeFile('svelte.config.js'); | ||
await waitForBuildWatchAndPageReload(); | ||
}); | ||
|
||
test('should detect changes in svelte config and rebuild', async () => { | ||
const injectPreprocessor = ({ content, filename }) => { | ||
if (filename && filename.includes('App.svelte')) { | ||
return { | ||
code: content.replace( | ||
'<!-- HMR-TEMPLATE-INJECT -->', | ||
'<div id="preprocess-inject">Injected</div>\n<!-- HMR-TEMPLATE-INJECT -->' | ||
) | ||
}; | ||
} | ||
}; | ||
await addFile( | ||
'svelte.config.js', | ||
`export default { | ||
preprocess:[{markup:${injectPreprocessor.toString()}}]};` | ||
); | ||
await waitForBuildWatchAndPageReload(); | ||
expect(await getText('#preprocess-inject')).toBe('Injected'); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
await (await getEl('#hmr-test-1 .increment')).click(); | ||
await sleep(50); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('1'); | ||
await updateHmrTest((content) => content.replace('color: red', 'color: green')); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('green'); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
await editFile('svelte.config.js', (content) => | ||
content | ||
.replace('preprocess-inject', 'preprocess-inject-2') | ||
.replace('Injected', 'Injected 2') | ||
); | ||
await waitForBuildWatchAndPageReload(); | ||
expect(await getText('#preprocess-inject-2')).toBe('Injected 2'); | ||
expect(await getEl('#preprocess-inject')).toBe(null); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('green'); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
await (await getEl('#hmr-test-1 .increment')).click(); | ||
await sleep(50); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('1'); | ||
await updateHmrTest((content) => content.replace('color: green', 'color: red')); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
await removeFile('svelte.config.js'); | ||
await waitForBuildWatchAndPageReload(); | ||
expect(await getEl('#preprocess-inject-2')).toBe(null); | ||
expect(await getEl('#preprocess-inject')).toBe(null); | ||
expect(await getColor('#hmr-test-1 .label')).toBe('red'); | ||
expect(await getText('#hmr-test-1 .counter')).toBe('0'); | ||
}); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" href="/favicon.png" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Svelte App</title> | ||
</head> | ||
<body> | ||
<script type="module" src="/src/index.js"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"name": "e2e-tests-build-watch", | ||
"private": true, | ||
"version": "0.0.0", | ||
"scripts": { | ||
"dev": "vite dev", | ||
"build": "vite build", | ||
"preview": "vite preview" | ||
}, | ||
"dependencies": { | ||
"e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple" | ||
}, | ||
"devDependencies": { | ||
"@sveltejs/vite-plugin-svelte": "workspace:^", | ||
"e2e-test-dep-vite-plugins": "file:../_test_dependencies/vite-plugins", | ||
"node-fetch": "^3.3.2", | ||
"svelte": "^5.36.13", | ||
"vite": "^7.0.5" | ||
}, | ||
"type": "module" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<script> | ||
import StaticImport from './components/StaticImport.svelte'; | ||
import Dependency from 'e2e-test-dep-svelte-simple'; | ||
import HmrTest from './components/HmrTest.svelte'; | ||
import PartialHmr from './components/partial-hmr/PartialHmr.svelte'; | ||
const jsTransform = '__JS_TRANSFORM_1__'; | ||
let dynamicImportComponent; | ||
function importDynamic() { | ||
import('./components/DynamicImport.svelte').then((m) => (dynamicImportComponent = m.default)); | ||
} | ||
</script> | ||
|
||
<h1 id="app-header">Test-App 2</h1> | ||
<!-- to be transformed into "Hello world!" text --> | ||
<p id="js-transform">{jsTransform}</p> | ||
<!-- to be transformed into "hello-world" class --> | ||
<p id="css-transform">Hello world</p> | ||
<StaticImport /> | ||
<Dependency /> | ||
{#if !dynamicImportComponent} | ||
<button id="button-import-dynamic" on:click={importDynamic}>import dynamic component</button> | ||
{:else} | ||
<svelte:component this={dynamicImportComponent} /> | ||
{/if} | ||
<HmrTest id="hmr-test-1" /> | ||
<HmrTest id="hmr-test-2" /> | ||
|
||
<!-- HMR-TEMPLATE-INJECT --> | ||
|
||
<PartialHmr /> | ||
|
||
<style> | ||
h1 { | ||
color: #111111; | ||
} | ||
|
||
:global(#css-transform) { | ||
color: __CSS_TRANSFORM_1__; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<script> | ||
import asset from '/src/assets/dynamic.png'; | ||
const importedAsset = asset; | ||
const label = 'dynamic-import'; | ||
</script> | ||
|
||
<div id="dynamic-import"> | ||
<span class="label">{label}</span> <img alt="imported" src={importedAsset} /> | ||
</div> | ||
|
||
<!-- HMR-TEMPLATE-INJECT --> | ||
<style> | ||
.label { | ||
padding-left: 2rem; | ||
background-image: url('/src/assets/dynamic.png'); | ||
background-repeat: no-repeat; | ||
background-size: contain; | ||
} | ||
img { | ||
width: 1rem; | ||
height: 1rem; | ||
} | ||
</style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
build --watch
was not tested at all before but 2 recent issues highlighted that we can't just depend on it being tested by vite itself