Skip to content

Commit

Permalink
fix: watch preprocessor dependencies (#34) (fixes #25)
Browse files Browse the repository at this point in the history
* wip: watch preprocessor dependencies

* wip: add tests for dependency watching

* wip: add tests for dependency watching

* fix: watch preprocessor dependencies and trigger hmr update on change (fixes #25)

* obey linter

* chore: update dependencies
  • Loading branch information
dominikg authored Apr 27, 2021
1 parent 4018ce6 commit e5d4749
Show file tree
Hide file tree
Showing 29 changed files with 492 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-baboons-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': patch
---

fix: watch preprocessor dependencies and trigger hmr on change
5 changes: 5 additions & 0 deletions packages/playground/svelte-preprocess/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.vscode
.idea
node_modules
dist
dist-ssr
50 changes: 50 additions & 0 deletions packages/playground/svelte-preprocess/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Svelte + TS + Vite

This template should help get you started developing with Svelte and TypeScript in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).

## Need an official Svelte framework?

Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.

## Technical considerations

**Why use this over SvelteKit?**

- SvelteKit is still a work-in-progress.
- It currently does not support the pure-SPA use case.
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.

This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-app` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.

Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.

**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**

Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.

**Why include `.vscode/extensions.json`?**

Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.

**Why enable `allowJs` in the TS template?**

While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.

**Why is HMR not preserving my local component state?**

HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).

If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.

```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store';
export default writable(0);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
isBuild,
getEl,
getText,
editFileAndWaitForHmrComplete,
untilUpdated,
sleep,
getColor,
addFile,
removeFile
} from '../../testUtils';

test('should render App', async () => {
expect(await getText('h1')).toBe(`I'm blue`);
expect(await getColor('h1')).toBe('blue');
expect(await getText('h2')).toBe(`I'm red`);
expect(await getColor('h2')).toBe('red');
expect(await getText('p')).toBe(`I'm green`);
expect(await getColor('p')).toBe('green');
expect(await getText('span')).toBe(`I'm orangered`);
expect(await getColor('span')).toBe('orangered');
});

test('should not have failed requests', async () => {
browserLogs.forEach((msg) => {
expect(msg).not.toMatch('404');
});
});

if (!isBuild) {
describe('hmr', () => {
test('should apply updates when editing App.svelte', async () => {
expect(await getText('span')).toBe(`I'm orangered`);
await editFileAndWaitForHmrComplete('src/App.svelte', (c) =>
c.replace(`I'm orangered`, `I'm replaced`)
);
expect(await getText('span')).toBe(`I'm replaced`);
expect(await getColor('span')).toBe('orangered');
await editFileAndWaitForHmrComplete(
'src/App.svelte',
(c) => c.replace(`color: orangered`, `color: magenta`),
'/src/App.svelte?svelte&type=style&lang.css'
);
expect(await getColor('span')).toBe('magenta');
});

test('should apply updates when editing MultiFile.html', async () => {
expect(await getText('h1')).toBe(`I'm blue`);
expect(await getText('h2')).toBe(`I'm red`);
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.html',
(c) => c.replace(`I'm blue`, `I'm replaced`).replace(`I'm red`, `I'm replaced too`),
'/src/lib/multifile/MultiFile.svelte'
);
expect(await getText('h1')).toBe(`I'm replaced`);
expect(await getText('h2')).toBe(`I'm replaced too`);
});

test('should apply updates when editing MultiFile.scss', async () => {
expect(await getColor('h1')).toBe('blue');
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.scss',
(c) => c.replace(`color: blue`, `color: magenta`),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h1')).toBe('magenta');
});

test('should apply updates when editing _someImport.scss', async () => {
expect(await getColor('h2')).toBe('red');
await editFileAndWaitForHmrComplete(
'src/lib/multifile/_someImport.scss',
(c) => c.replace(`color: red`, `color: magenta`),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h2')).toBe('magenta');
});

test('should remove styles that are no longer imported', async () => {
expect(await getColor('h2')).toBe('magenta');
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.scss',
(c) => c.replace(`@import 'someImport';`, `/*@import 'someImport';*/`),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h2')).toBe('black');
});

test('should apply styles from new dependency', async () => {
expect(await getColor('h2')).toBe('black');
await addFile('src/lib/multifile/_foo.scss', 'h2 { color: maroon; }');
expect(await getColor('h2')).toBe('black');
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.scss',
(c) => c.replace(`/*@import 'someImport';*/`, `/*@import 'someImport';*/\n@import 'foo';`),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h2')).toBe('maroon');
await editFileAndWaitForHmrComplete(
'src/lib/multifile/_foo.scss',
(c) => c.replace(`maroon`, `green`),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h2')).toBe('green');
});

test('should apply updates when editing MultiFile.ts', async () => {
expect(await getText('p')).toBe(`I'm green`);
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.ts',
(c) => c.replace(`'green'`, `'a replaced value'`),
'/src/lib/multifile/MultiFile.svelte'
);
expect(await getText('p')).toBe(`I'm a replaced value`);
});

test('should apply updates when editing someother.css', async () => {
expect(await getColor('p')).toBe('green');
await editFileAndWaitForHmrComplete('src/lib/multifile/someother.css', (c) =>
c.replace(`color: green`, `color: magenta`)
);
expect(await getColor('p')).toBe('magenta');
});

test('should show error on deleting dependency', async () => {
expect(await getColor('h2')).toBe('green');
await removeFile('src/lib/multifile/_foo.scss');
expect(await getColor('h2')).toBe('green');
const errorOverlay = await page.waitForSelector('vite-error-overlay');
expect(errorOverlay).toBeTruthy();
await errorOverlay.click({ position: { x: 1, y: 1 } });
await sleep(50);
const errorOverlay2 = await getEl('vite-error-overlay');
expect(errorOverlay2).toBeFalsy();
await editFileAndWaitForHmrComplete(
'src/lib/multifile/MultiFile.scss',
(c) => c.replace(`@import 'foo';`, ``),
'/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css'
);
expect(await getColor('h2')).toBe('black');
});
});
}
13 changes: 13 additions & 0 deletions packages/playground/svelte-preprocess/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte + TS + Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
17 changes: 17 additions & 0 deletions packages/playground/svelte-preprocess/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "playground-svelte-preprocess",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "workspace:*",
"svelte": "^3.37.0",
"svelte-preprocess": "^4.7.2",
"typescript": "^4.2.4",
"vite": "^2.2.3"
}
}
Binary file not shown.
16 changes: 16 additions & 0 deletions packages/playground/svelte-preprocess/src/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import logo from './assets/svelte.png';
import MultiFile from './lib/multifile/MultiFile.svelte';
</script>

<main>
<img src={logo} alt="Svelte Logo" />
<MultiFile />
<span>I'm orangered</span>
</main>

<style>
span {
color: orangered;
}
</style>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/playground/svelte-preprocess/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h1>I'm blue</h1>
<h2>I'm red</h2>
<p>I'm {foo}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import 'someImport';
h1 {
color: blue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts" src="./MultiFile.ts"></script>

<template lang="html" src="./MultiFile.html" />

<style lang="scss" src="./MultiFile.scss"></style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line prefer-const
import './someother.css';
export const foo: string = 'green';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h2 {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
p {
color: green;
}
7 changes: 7 additions & 0 deletions packages/playground/svelte-preprocess/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import App from './App.svelte';

const app = new App({
target: document.getElementById('app')
});

export default app;
7 changes: 7 additions & 0 deletions packages/playground/svelte-preprocess/svelte.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const sveltePreprocess = require('svelte-preprocess')

module.exports = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
}
36 changes: 36 additions & 0 deletions packages/playground/svelte-preprocess/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}
12 changes: 12 additions & 0 deletions packages/playground/svelte-preprocess/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { defineConfig } = require('vite');
const svelte = require('@sveltejs/vite-plugin-svelte');

module.exports = defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
plugins: [svelte()],
build: {
minify: isProduction
}
};
});
9 changes: 6 additions & 3 deletions packages/playground/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,16 @@ export async function hmrUpdateComplete(file, timeout) {
});
}

export async function editFileAndWaitForHmrComplete(file, replacer) {
export async function editFileAndWaitForHmrComplete(file, replacer, fileUpdateToWaitFor?) {
const newContent = await editFile(file, replacer);
if (!fileUpdateToWaitFor) {
fileUpdateToWaitFor = file;
}
try {
await hmrUpdateComplete(file, 10000);
await hmrUpdateComplete(fileUpdateToWaitFor, 10000);
} catch (e) {
console.log(`retrying hmr update for ${file}`);
await editFile(file, () => newContent);
await hmrUpdateComplete(file, 5000);
await hmrUpdateComplete(fileUpdateToWaitFor, 5000);
}
}
12 changes: 6 additions & 6 deletions packages/playground/vite-ssr/__tests__/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,21 @@ exports.serve = async function serve(root, isProd) {
port: port,
async close() {
let err;
if(server) {
if (server) {
err = await new Promise((resolve) => {
server.close(resolve)
})
server.close(resolve);
});
}
if (vite) {
try {
await vite.close()
await vite.close();
} catch (e) {
if(!err) {
if (!err) {
err = e;
}
}
}
if(err) {
if (err) {
throw err;
}
}
Expand Down
Loading

0 comments on commit e5d4749

Please sign in to comment.