|
| 1 | +/** |
| 2 | + * Run this script to convert the project to TypeScript. This is only guaranteed to work |
| 3 | + * on the unmodified default template; if you have done code changes you are likely need |
| 4 | + * to touch up the generated project manually. |
| 5 | + */ |
| 6 | + |
| 7 | +// @ts-check |
| 8 | +const fs = require('fs'); |
| 9 | +const path = require('path'); |
| 10 | +const { argv } = require('process'); |
| 11 | + |
| 12 | +const projectRoot = argv[2] || path.join(__dirname, '..'); |
| 13 | + |
| 14 | +const isRollup = fs.existsSync(path.join(projectRoot, "rollup.config.js")); |
| 15 | + |
| 16 | +function warn(message) { |
| 17 | + console.warn('Warning: ' + message); |
| 18 | +} |
| 19 | + |
| 20 | +function replaceInFile(fileName, replacements) { |
| 21 | + if (fs.existsSync(fileName)) { |
| 22 | + let contents = fs.readFileSync(fileName, 'utf8'); |
| 23 | + let hadUpdates = false; |
| 24 | + |
| 25 | + replacements.forEach(([from, to]) => { |
| 26 | + const newContents = contents.replace(from, to); |
| 27 | + |
| 28 | + const isAlreadyApplied = typeof to !== 'string' || contents.includes(to); |
| 29 | + |
| 30 | + if (newContents !== contents) { |
| 31 | + contents = newContents; |
| 32 | + hadUpdates = true; |
| 33 | + } else if (!isAlreadyApplied) { |
| 34 | + warn(`Wanted to update "${from}" in ${fileName}, but did not find it.`); |
| 35 | + } |
| 36 | + }); |
| 37 | + |
| 38 | + if (hadUpdates) { |
| 39 | + fs.writeFileSync(fileName, contents); |
| 40 | + } else { |
| 41 | + console.log(`${fileName} had already been updated.`); |
| 42 | + } |
| 43 | + } else { |
| 44 | + warn(`Wanted to update ${fileName} but the file did not exist.`); |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +function createFile(fileName, contents) { |
| 49 | + if (fs.existsSync(fileName)) { |
| 50 | + warn(`Wanted to create ${fileName}, but it already existed. Leaving existing file.`); |
| 51 | + } else { |
| 52 | + fs.writeFileSync(fileName, contents); |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +function addDepsToPackageJson() { |
| 57 | + const pkgJSONPath = path.join(projectRoot, 'package.json'); |
| 58 | + const packageJSON = JSON.parse(fs.readFileSync(pkgJSONPath, 'utf8')); |
| 59 | + packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { |
| 60 | + ...(isRollup ? { '@rollup/plugin-typescript': '^6.0.0' } : { 'ts-loader': '^8.0.4' }), |
| 61 | + '@tsconfig/svelte': '^1.0.10', |
| 62 | + '@types/compression': '^1.7.0', |
| 63 | + '@types/node': '^14.11.1', |
| 64 | + '@types/polka': '^0.5.1', |
| 65 | + 'svelte-check': '^1.0.46', |
| 66 | + 'svelte-preprocess': '^4.3.0', |
| 67 | + tslib: '^2.0.1', |
| 68 | + typescript: '^4.0.3' |
| 69 | + }); |
| 70 | + |
| 71 | + // Add script for checking |
| 72 | + packageJSON.scripts = Object.assign(packageJSON.scripts, { |
| 73 | + validate: 'svelte-check --ignore src/node_modules/@sapper' |
| 74 | + }); |
| 75 | + |
| 76 | + // Write the package JSON |
| 77 | + fs.writeFileSync(pkgJSONPath, JSON.stringify(packageJSON, null, ' ')); |
| 78 | +} |
| 79 | + |
| 80 | +function changeJsExtensionToTs(dir) { |
| 81 | + const elements = fs.readdirSync(dir, { withFileTypes: true }); |
| 82 | + |
| 83 | + for (let i = 0; i < elements.length; i++) { |
| 84 | + if (elements[i].isDirectory()) { |
| 85 | + changeJsExtensionToTs(path.join(dir, elements[i].name)); |
| 86 | + } else if (elements[i].name.match(/^[^_]((?!json).)*js$/)) { |
| 87 | + fs.renameSync(path.join(dir, elements[i].name), path.join(dir, elements[i].name).replace('.js', '.ts')); |
| 88 | + } |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +function updateSingleSvelteFile({ view, vars, contextModule }) { |
| 93 | + replaceInFile(path.join(projectRoot, 'src', `${view}.svelte`), [ |
| 94 | + [/(?:<script)(( .*?)*?)>/gm, (m, attrs) => `<script${attrs}${!attrs.includes('lang="ts"') ? ' lang="ts"' : ''}>`], |
| 95 | + ...(vars ? vars.map(({ name, type }) => [`export let ${name};`, `export let ${name}: ${type};`]) : []), |
| 96 | + ...(contextModule ? contextModule.map(({ js, ts }) => [js, ts]) : []) |
| 97 | + ]); |
| 98 | +} |
| 99 | + |
| 100 | +// Switch the *.svelte file to use TS |
| 101 | +function updateSvelteFiles() { |
| 102 | + [ |
| 103 | + { |
| 104 | + view: 'components/Nav', |
| 105 | + vars: [{ name: 'segment', type: 'string' }] |
| 106 | + }, |
| 107 | + { |
| 108 | + view: 'routes/_layout', |
| 109 | + vars: [{ name: 'segment', type: 'string' }] |
| 110 | + }, |
| 111 | + { |
| 112 | + view: 'routes/_error', |
| 113 | + vars: [ |
| 114 | + { name: 'status', type: 'number' }, |
| 115 | + { name: 'error', type: 'Error' } |
| 116 | + ] |
| 117 | + }, |
| 118 | + { |
| 119 | + view: 'routes/blog/index', |
| 120 | + vars: [{ name: 'posts', type: '{ slug: string; title: string, html: any }[]' }], |
| 121 | + contextModule: [ |
| 122 | + { |
| 123 | + js: '.then(r => r.json())', |
| 124 | + ts: '.then((r: { json: () => any; }) => r.json())' |
| 125 | + }, |
| 126 | + { |
| 127 | + js: '.then(posts => {', |
| 128 | + ts: '.then((posts: { slug: string; title: string, html: any }[]) => {' |
| 129 | + } |
| 130 | + ] |
| 131 | + }, |
| 132 | + { |
| 133 | + view: 'routes/blog/[slug]', |
| 134 | + vars: [{ name: 'post', type: '{ slug: string; title: string, html: any }' }] |
| 135 | + } |
| 136 | + ].forEach(updateSingleSvelteFile); |
| 137 | +} |
| 138 | + |
| 139 | +function updateRollupConfig() { |
| 140 | + // Edit rollup config |
| 141 | + replaceInFile(path.join(projectRoot, 'rollup.config.js'), [ |
| 142 | + // Edit imports |
| 143 | + [ |
| 144 | + /'rollup-plugin-terser';\n(?!import sveltePreprocess)/, |
| 145 | + `'rollup-plugin-terser'; |
| 146 | +import sveltePreprocess from 'svelte-preprocess'; |
| 147 | +import typescript from '@rollup/plugin-typescript'; |
| 148 | +` |
| 149 | + ], |
| 150 | + // Edit inputs |
| 151 | + [ |
| 152 | + /(?<!THIS_IS_UNDEFINED[^\n]*\n\s*)onwarn\(warning\);/, |
| 153 | + `(warning.code === 'THIS_IS_UNDEFINED') ||\n\tonwarn(warning);` |
| 154 | + ], |
| 155 | + [/input: config.client.input\(\)(?!\.replace)/, `input: config.client.input().replace(/\\.js$/, '.ts')`], |
| 156 | + [ |
| 157 | + /input: config.server.input\(\)(?!\.replace)/, |
| 158 | + `input: { server: config.server.input().server.replace(/\\.js$/, ".ts") }` |
| 159 | + ], |
| 160 | + [ |
| 161 | + /input: config.serviceworker.input\(\)(?!\.replace)/, |
| 162 | + `input: config.serviceworker.input().replace(/\\.js$/, '.ts')` |
| 163 | + ], |
| 164 | + // Add preprocess |
| 165 | + [/compilerOptions/g, 'preprocess: sveltePreprocess({ sourceMap: dev }),\n\t\t\t\tcompilerOptions'], |
| 166 | + // Add TypeScript |
| 167 | + [/commonjs\(\)(?!,\n\s*typescript)/g, 'commonjs(),\n\t\t\ttypescript({ sourceMap: dev })'] |
| 168 | + ]); |
| 169 | +} |
| 170 | + |
| 171 | +function updateWebpackConfig() { |
| 172 | + // Edit webpack config |
| 173 | + replaceInFile(path.join(projectRoot, 'webpack.config.js'), [ |
| 174 | + // Edit imports |
| 175 | + [ |
| 176 | + /require\('webpack-modules'\);\n(?!const sveltePreprocess)/, |
| 177 | + `require('webpack-modules');\nconst sveltePreprocess = require('svelte-preprocess');\n` |
| 178 | + ], |
| 179 | + // Edit extensions |
| 180 | + [ |
| 181 | + /\['\.mjs', '\.js', '\.json', '\.svelte', '\.html'\]/, |
| 182 | + `['.mjs', '.js', '.ts', '.json', '.svelte', '.html']` |
| 183 | + ], |
| 184 | + // Edit entries |
| 185 | + [ |
| 186 | + /entry: config\.client\.entry\(\)/, |
| 187 | + `entry: { main: config.client.entry().main.replace(/\\.js$/, '.ts') }` |
| 188 | + ], |
| 189 | + [ |
| 190 | + /entry: config\.server\.entry\(\)/, |
| 191 | + `entry: { server: config.server.entry().server.replace(/\\.js$/, '.ts') }` |
| 192 | + ], |
| 193 | + [ |
| 194 | + /entry: config\.serviceworker\.entry\(\)/, |
| 195 | + `entry: { 'service-worker': config.serviceworker.entry()['service-worker'].replace(/\\.js$/, '.ts') }` |
| 196 | + ], |
| 197 | + [ |
| 198 | + /loader: 'svelte-loader',\n\t\t\t\t\t\toptions: {/g, |
| 199 | + 'loader: \'svelte-loader\',\n\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\tpreprocess: sveltePreprocess({ sourceMap: dev }),' |
| 200 | + ], |
| 201 | + // Add TypeScript rules for client and server |
| 202 | + [ |
| 203 | + /module: {\n\s*rules: \[\n\s*(?!{\n\s*test: \/\\\.ts\$\/)/g, |
| 204 | + `module: {\n\t\t\trules: [\n\t\t\t\t{\n\t\t\t\t\ttest: /\\.ts$/,\n\t\t\t\t\tloader: 'ts-loader'\n\t\t\t\t},\n\t\t\t\t` |
| 205 | + ], |
| 206 | + // Add TypeScript rules for serviceworker |
| 207 | + [ |
| 208 | + /output: config\.serviceworker\.output\(\),\n\s*(?!module)/, |
| 209 | + `output: config.serviceworker.output(),\n\t\tmodule: {\n\t\t\trules: [\n\t\t\t\t{\n\t\t\t\t\ttest: /\\.ts$/,\n\t\t\t\t\tloader: 'ts-loader'\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t` |
| 210 | + ], |
| 211 | + // Edit outputs |
| 212 | + [ |
| 213 | + /output: config\.serviceworker\.output\(\),\n\s*(?!resolve)/, |
| 214 | + `output: config.serviceworker.output(),\n\t\tresolve: { extensions: ['.mjs', '.js', '.ts', '.json'] },\n\t\t` |
| 215 | + ] |
| 216 | + ]); |
| 217 | +} |
| 218 | + |
| 219 | +function updateServiceWorker() { |
| 220 | + replaceInFile(path.join(projectRoot, 'src', 'service-worker.ts'), [ |
| 221 | + [`shell.concat(files);`, `(shell as string[]).concat(files as string[]);`], |
| 222 | + [`self.skipWaiting();`, `((self as any) as ServiceWorkerGlobalScope).skipWaiting();`], |
| 223 | + [`self.clients.claim();`, `((self as any) as ServiceWorkerGlobalScope).clients.claim();`], |
| 224 | + [`fetchAndCache(request)`, `fetchAndCache(request: Request)`], |
| 225 | + [`self.addEventListener('activate', event =>`, `self.addEventListener('activate', (event: ExtendableEvent) =>`], |
| 226 | + [`self.addEventListener('install', event =>`, `self.addEventListener('install', (event: ExtendableEvent) =>`], |
| 227 | + [`addEventListener('fetch', event =>`, `addEventListener('fetch', (event: FetchEvent) =>`], |
| 228 | + ]); |
| 229 | +} |
| 230 | + |
| 231 | +function createTsConfig() { |
| 232 | + const tsconfig = `{ |
| 233 | + "extends": "@tsconfig/svelte/tsconfig.json", |
| 234 | + "compilerOptions": { |
| 235 | + "lib": ["DOM", "ES2017", "WebWorker"] |
| 236 | + }, |
| 237 | + "include": ["src/**/*", "src/node_modules/**/*"], |
| 238 | + "exclude": ["node_modules/*", "__sapper__/*", "static/*"] |
| 239 | + }`; |
| 240 | + |
| 241 | + createFile(path.join(projectRoot, 'tsconfig.json'), tsconfig); |
| 242 | +} |
| 243 | + |
| 244 | +// Adds the extension recommendation |
| 245 | +function configureVsCode() { |
| 246 | + const dir = path.join(projectRoot, '.vscode'); |
| 247 | + |
| 248 | + if (!fs.existsSync(dir)) { |
| 249 | + fs.mkdirSync(dir); |
| 250 | + } |
| 251 | + |
| 252 | + createFile(path.join(projectRoot, '.vscode', 'extensions.json'), `{"recommendations": ["svelte.svelte-vscode"]}`); |
| 253 | +} |
| 254 | + |
| 255 | +function deleteThisScript() { |
| 256 | + fs.unlinkSync(path.join(__filename)); |
| 257 | + |
| 258 | + // Check for Mac's DS_store file, and if it's the only one left remove it |
| 259 | + const remainingFiles = fs.readdirSync(path.join(__dirname)); |
| 260 | + if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') { |
| 261 | + fs.unlinkSync(path.join(__dirname, '.DS_store')); |
| 262 | + } |
| 263 | + |
| 264 | + // Check if the scripts folder is empty |
| 265 | + if (fs.readdirSync(path.join(__dirname)).length === 0) { |
| 266 | + // Remove the scripts folder |
| 267 | + fs.rmdirSync(path.join(__dirname)); |
| 268 | + } |
| 269 | +} |
| 270 | + |
| 271 | +console.log(`Adding TypeScript with ${isRollup ? "Rollup" : "webpack" }...`); |
| 272 | + |
| 273 | +addDepsToPackageJson(); |
| 274 | + |
| 275 | +changeJsExtensionToTs(path.join(projectRoot, 'src')); |
| 276 | + |
| 277 | +updateSvelteFiles(); |
| 278 | + |
| 279 | +if (isRollup) { |
| 280 | + updateRollupConfig(); |
| 281 | +} else { |
| 282 | + updateWebpackConfig(); |
| 283 | +} |
| 284 | + |
| 285 | +updateServiceWorker(); |
| 286 | + |
| 287 | +createTsConfig(); |
| 288 | + |
| 289 | +configureVsCode(); |
| 290 | + |
| 291 | +// Delete this script, but not during testing |
| 292 | +if (!argv[2]) { |
| 293 | + deleteThisScript(); |
| 294 | +} |
| 295 | + |
| 296 | +console.log('Converted to TypeScript.'); |
| 297 | + |
| 298 | +if (fs.existsSync(path.join(projectRoot, 'node_modules'))) { |
| 299 | + console.log(` |
| 300 | +Next: |
| 301 | +1. run 'npm install' again to install TypeScript dependencies |
| 302 | +2. run 'npm run build' for the @sapper imports in your project to work |
| 303 | +`); |
| 304 | +} |
0 commit comments