UNPKG

14 kBJavaScriptView Raw
1import { existsSync } from 'fs';
2import sade from 'sade';
3import { pathToFileURL, resolve as resolve$1 } from 'url';
4import path from 'path';
5
6let FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM, isTTY=true;
7if (typeof process !== 'undefined') {
8 ({ FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM } = process.env);
9 isTTY = process.stdout && process.stdout.isTTY;
10}
11
12const $ = {
13 enabled: !NODE_DISABLE_COLORS && NO_COLOR == null && TERM !== 'dumb' && (
14 FORCE_COLOR != null && FORCE_COLOR !== '0' || isTTY
15 ),
16
17 // modifiers
18 reset: init(0, 0),
19 bold: init(1, 22),
20 dim: init(2, 22),
21 italic: init(3, 23),
22 underline: init(4, 24),
23 inverse: init(7, 27),
24 hidden: init(8, 28),
25 strikethrough: init(9, 29),
26
27 // colors
28 black: init(30, 39),
29 red: init(31, 39),
30 green: init(32, 39),
31 yellow: init(33, 39),
32 blue: init(34, 39),
33 magenta: init(35, 39),
34 cyan: init(36, 39),
35 white: init(37, 39),
36 gray: init(90, 39),
37 grey: init(90, 39),
38
39 // background colors
40 bgBlack: init(40, 49),
41 bgRed: init(41, 49),
42 bgGreen: init(42, 49),
43 bgYellow: init(43, 49),
44 bgBlue: init(44, 49),
45 bgMagenta: init(45, 49),
46 bgCyan: init(46, 49),
47 bgWhite: init(47, 49)
48};
49
50function run(arr, str) {
51 let i=0, tmp, beg='', end='';
52 for (; i < arr.length; i++) {
53 tmp = arr[i];
54 beg += tmp.open;
55 end += tmp.close;
56 if (!!~str.indexOf(tmp.close)) {
57 str = str.replace(tmp.rgx, tmp.close + tmp.open);
58 }
59 }
60 return beg + str + end;
61}
62
63function chain(has, keys) {
64 let ctx = { has, keys };
65
66 ctx.reset = $.reset.bind(ctx);
67 ctx.bold = $.bold.bind(ctx);
68 ctx.dim = $.dim.bind(ctx);
69 ctx.italic = $.italic.bind(ctx);
70 ctx.underline = $.underline.bind(ctx);
71 ctx.inverse = $.inverse.bind(ctx);
72 ctx.hidden = $.hidden.bind(ctx);
73 ctx.strikethrough = $.strikethrough.bind(ctx);
74
75 ctx.black = $.black.bind(ctx);
76 ctx.red = $.red.bind(ctx);
77 ctx.green = $.green.bind(ctx);
78 ctx.yellow = $.yellow.bind(ctx);
79 ctx.blue = $.blue.bind(ctx);
80 ctx.magenta = $.magenta.bind(ctx);
81 ctx.cyan = $.cyan.bind(ctx);
82 ctx.white = $.white.bind(ctx);
83 ctx.gray = $.gray.bind(ctx);
84 ctx.grey = $.grey.bind(ctx);
85
86 ctx.bgBlack = $.bgBlack.bind(ctx);
87 ctx.bgRed = $.bgRed.bind(ctx);
88 ctx.bgGreen = $.bgGreen.bind(ctx);
89 ctx.bgYellow = $.bgYellow.bind(ctx);
90 ctx.bgBlue = $.bgBlue.bind(ctx);
91 ctx.bgMagenta = $.bgMagenta.bind(ctx);
92 ctx.bgCyan = $.bgCyan.bind(ctx);
93 ctx.bgWhite = $.bgWhite.bind(ctx);
94
95 return ctx;
96}
97
98function init(open, close) {
99 let blk = {
100 open: `\x1b[${open}m`,
101 close: `\x1b[${close}m`,
102 rgx: new RegExp(`\\x1b\\[${close}m`, 'g')
103 };
104 return function (txt) {
105 if (this !== void 0 && this.has !== void 0) {
106 !!~this.has.indexOf(open) || (this.has.push(open),this.keys.push(blk));
107 return txt === void 0 ? this : $.enabled ? run(this.keys, txt+'') : txt+'';
108 }
109 return txt === void 0 ? chain([open], [blk]) : $.enabled ? run([blk], txt+'') : txt+'';
110 };
111}
112
113const noop = () => {};
114
115/** @typedef {import('./types').ConfigDefinition} ConfigDefinition */
116
117/** @type {Record<string, ConfigDefinition>} */
118const options = {
119 compilerOptions: {
120 type: 'leaf',
121 default: null,
122 validate: noop
123 },
124
125 extensions: {
126 type: 'leaf',
127 default: ['.svelte'],
128 validate: (option, keypath) => {
129 if (!Array.isArray(option) || !option.every((page) => typeof page === 'string')) {
130 throw new Error(`${keypath} must be an array of strings`);
131 }
132
133 option.forEach((extension) => {
134 if (extension[0] !== '.') {
135 throw new Error(`Each member of ${keypath} must start with '.' — saw '${extension}'`);
136 }
137
138 if (!/^(\.[a-z0-9]+)+$/i.test(extension)) {
139 throw new Error(`File extensions must be alphanumeric — saw '${extension}'`);
140 }
141 });
142
143 return option;
144 }
145 },
146
147 kit: {
148 type: 'branch',
149 children: {
150 adapter: {
151 type: 'leaf',
152 default: [null],
153 validate: (option, keypath) => {
154 // support both `adapter: 'foo'` and `adapter: ['foo', opts]`
155 if (!Array.isArray(option)) {
156 option = [option];
157 }
158
159 // TODO allow inline functions
160 assert_is_string(option[0], keypath);
161
162 return option;
163 }
164 },
165
166 amp: expect_boolean(false),
167
168 appDir: expect_string('_app', false),
169
170 files: {
171 type: 'branch',
172 children: {
173 assets: expect_string('static'),
174 lib: expect_string('src/lib'),
175 routes: expect_string('src/routes'),
176 serviceWorker: expect_string('src/service-worker'),
177 setup: expect_string('src/setup'),
178 template: expect_string('src/app.html')
179 }
180 },
181
182 host: expect_string(null),
183
184 hostHeader: expect_string(null),
185
186 paths: {
187 type: 'branch',
188 children: {
189 base: expect_string(''),
190 assets: expect_string('')
191 }
192 },
193
194 prerender: {
195 type: 'branch',
196 children: {
197 crawl: expect_boolean(true),
198 enabled: expect_boolean(true),
199 force: expect_boolean(false),
200 pages: {
201 type: 'leaf',
202 default: ['*'],
203 validate: (option, keypath) => {
204 if (!Array.isArray(option) || !option.every((page) => typeof page === 'string')) {
205 throw new Error(`${keypath} must be an array of strings`);
206 }
207
208 option.forEach((page) => {
209 if (page !== '*' && page[0] !== '/') {
210 throw new Error(
211 `Each member of ${keypath} must be either '*' or an absolute path beginning with '/' — saw '${page}'`
212 );
213 }
214 });
215
216 return option;
217 }
218 }
219 }
220 },
221
222 // used for testing
223 startGlobal: expect_string(null),
224
225 target: expect_string(null)
226 }
227 },
228
229 preprocess: {
230 type: 'leaf',
231 default: null,
232 validate: noop
233 }
234};
235
236/**
237 * @param {string} string
238 * @param {boolean} allow_empty
239 * @returns {ConfigDefinition}
240 */
241function expect_string(string, allow_empty = true) {
242 return {
243 type: 'leaf',
244 default: string,
245 validate: (option, keypath) => {
246 assert_is_string(option, keypath);
247 if (!allow_empty && option === '') {
248 throw new Error(`${keypath} cannot be empty`);
249 }
250 return option;
251 }
252 };
253}
254
255/**
256 * @param {boolean} boolean
257 * @returns {ConfigDefinition}
258 */
259function expect_boolean(boolean) {
260 return {
261 type: 'leaf',
262 default: boolean,
263 validate: (option, keypath) => {
264 if (typeof option !== 'boolean') {
265 throw new Error(`${keypath} should be true or false, if specified`);
266 }
267 return option;
268 }
269 };
270}
271
272/**
273 * @param {any} option
274 * @param {string} keypath
275 */
276function assert_is_string(option, keypath) {
277 if (typeof option !== 'string') {
278 throw new Error(`${keypath} should be a string, if specified`);
279 }
280}
281
282/** @typedef {import('./types').ConfigDefinition} ConfigDefinition */
283
284/**
285 * @param {Record<string, ConfigDefinition>} definition
286 * @param {any} option
287 * @param {string} keypath
288 * @returns {any}
289 */
290function validate(definition, option, keypath) {
291 for (const key in option) {
292 if (!(key in definition)) {
293 let message = `Unexpected option ${keypath}.${key}`;
294
295 if (keypath === 'config' && key in options.kit) {
296 message += ` (did you mean config.kit.${key}?)`;
297 } else if (keypath === 'config.kit' && key in options) {
298 message += ` (did you mean config.${key}?)`;
299 }
300
301 throw new Error(message);
302 }
303 }
304
305 /** @type {Record<string, any>} */
306 const merged = {};
307
308 for (const key in definition) {
309 const expected = definition[key];
310 const actual = option[key];
311
312 const child_keypath = `${keypath}.${key}`;
313
314 if (key in option) {
315 if (expected.type === 'branch') {
316 if (actual && (typeof actual !== 'object' || Array.isArray(actual))) {
317 throw new Error(`${keypath}.${key} should be an object`);
318 }
319
320 merged[key] = validate(expected.children, actual, child_keypath);
321 } else {
322 merged[key] = expected.validate(actual, child_keypath);
323 }
324 } else {
325 merged[key] =
326 expected.type === 'branch'
327 ? validate(expected.children, {}, child_keypath)
328 : expected.default;
329 }
330 }
331
332 return merged;
333}
334
335/**
336 * @param {string} from
337 * @param {string} to
338 */
339function resolve(from, to) {
340 // the `/.` is weird, but allows `${assets}/images/blah.jpg` to work
341 // when `assets` is empty
342 return remove_trailing_slash(resolve$1(add_trailing_slash(from), to)) || '/.';
343}
344
345/**
346 * @param {string} str
347 */
348function add_trailing_slash(str) {
349 return str.endsWith('/') ? str : `${str}/`;
350}
351
352/**
353 * @param {string} str
354 */
355function remove_trailing_slash(str) {
356 return str.endsWith('/') ? str.slice(0, -1) : str;
357}
358
359async function load_config({ cwd = process.cwd() } = {}) {
360 const config_file = path.join(cwd, 'svelte.config.cjs');
361 const config = await import(pathToFileURL(config_file).href);
362 const validated = validate_config(config.default);
363
364 validated.kit.files.assets = path.resolve(cwd, validated.kit.files.assets);
365 validated.kit.files.lib = path.resolve(cwd, validated.kit.files.lib);
366 validated.kit.files.routes = path.resolve(cwd, validated.kit.files.routes);
367 validated.kit.files.serviceWorker = path.resolve(cwd, validated.kit.files.serviceWorker);
368 validated.kit.files.setup = path.resolve(cwd, validated.kit.files.setup);
369 validated.kit.files.template = path.resolve(cwd, validated.kit.files.template);
370
371 // TODO check all the `files` exist when the config is loaded?
372 // TODO check that `target` is present in the provided template
373
374 return validated;
375}
376
377/**
378 * @param {import('../../../types').Config} config
379 * @returns {import('../../types.js').ValidatedConfig}
380 */
381function validate_config(config) {
382 /** @type {import('../../types.js').ValidatedConfig} */
383 const validated = validate(options, config, 'config');
384
385 // resolve paths
386 const { paths } = validated.kit;
387
388 if (paths.base !== '' && !paths.base.startsWith('/')) {
389 throw new Error('config.kit.paths.base must be a root-relative path');
390 }
391
392 paths.assets = resolve(paths.base, paths.assets);
393
394 return validated;
395}
396
397async function get_config() {
398 // TODO this is temporary, for the benefit of early adopters
399 if (existsSync('snowpack.config.js') || existsSync('snowpack.config.cjs')) {
400 // prettier-ignore
401 console.error($.bold().red(
402 'SvelteKit now uses https://vitejs.dev. Please replace snowpack.config.js with vite.config.js:'
403 ));
404
405 // prettier-ignore
406 console.error(`
407 // Consult https://vitejs.dev/config/ to learn about these options
408 import { join } from 'path';
409 import { readFileSync } from 'fs';
410 import { cwd } from 'process';
411
412 const pkg = JSON.parse(readFileSync(join(cwd(), 'package.json')));
413
414 /** @type {import('vite').UserConfig} */
415 export default {
416 ssr: {
417 noExternal: Object.keys(pkg.dependencies || {})
418 }
419 };
420
421 `.replace(/^\t{3}/gm, '').replace(/\t/gm, ' ').trim());
422 }
423
424 try {
425 return await load_config();
426 } catch (error) {
427 let message = error.message;
428
429 if (error.code === 'MODULE_NOT_FOUND') {
430 if (existsSync('svelte.config.js')) {
431 // TODO this is temporary, for the benefit of early adopters
432 message =
433 'You must rename svelte.config.js to svelte.config.cjs, and snowpack.config.js to snowpack.config.cjs';
434 } else {
435 message = 'Missing svelte.config.cjs';
436 }
437 } else if (error.name === 'SyntaxError') {
438 message = 'Malformed svelte.config.cjs';
439 }
440
441 console.error($.bold().red(message));
442 console.error($.grey(error.stack));
443 process.exit(1);
444 }
445}
446
447/** @param {Error} error */
448function handle_error(error) {
449 console.log($.bold().red(`> ${error.message}`));
450 console.log($.gray(error.stack));
451 process.exit(1);
452}
453
454/** @param {number} port */
455async function launch(port) {
456 const { exec } = await import('child_process');
457 exec(`${process.platform == 'win32' ? 'start' : 'open'} http://localhost:${port}`);
458}
459
460const prog = sade('svelte-kit').version('1.0.0-next.48');
461
462prog
463 .command('dev')
464 .describe('Start a development server')
465 .option('-p, --port', 'Port', 3000)
466 .option('-o, --open', 'Open a browser tab', false)
467 .action(async ({ port, open }) => {
468 process.env.NODE_ENV = 'development';
469 const config = await get_config();
470
471 const { dev } = await import('./chunks/index.js');
472
473 try {
474 const watcher = await dev({ port, config });
475
476 watcher.on('stdout', (data) => {
477 process.stdout.write(data);
478 });
479
480 watcher.on('stderr', (data) => {
481 process.stderr.write(data);
482 });
483
484 console.log($.bold().cyan(`> Listening on http://localhost:${watcher.port}`));
485 if (open) launch(watcher.port);
486 } catch (error) {
487 handle_error(error);
488 }
489 });
490
491prog
492 .command('build')
493 .describe('Create a production build of your app')
494 .option('--verbose', 'Log more stuff', false)
495 .action(async ({ verbose }) => {
496 process.env.NODE_ENV = 'production';
497 const config = await get_config();
498
499 try {
500 const { build } = await import('./chunks/index4.js');
501 await build(config);
502
503 console.log(`\nRun ${$.bold().cyan('npm start')} to try your app locally.`);
504
505 if (config.kit.adapter[0]) {
506 const { adapt } = await import('./chunks/index5.js');
507 await adapt(config, { verbose });
508 } else {
509 console.log($.bold().yellow('\nNo adapter specified'));
510
511 // prettier-ignore
512 console.log(
513 `See ${$.bold().cyan('https://kit.svelte.dev/docs#adapters')} to learn how to configure your app to run on the platform of your choosing`
514 );
515 }
516 } catch (error) {
517 handle_error(error);
518 }
519 });
520
521prog
522 .command('start')
523 .describe('Serve an already-built app')
524 .option('-p, --port', 'Port', 3000)
525 .option('-o, --open', 'Open a browser tab', false)
526 .action(async ({ port, open }) => {
527 process.env.NODE_ENV = 'production';
528 const config = await get_config();
529
530 const { start } = await import('./chunks/index6.js');
531
532 try {
533 await start({ port, config });
534
535 console.log($.bold().cyan(`> Listening on http://localhost:${port}`));
536 if (open) if (open) launch(port);
537 } catch (error) {
538 handle_error(error);
539 }
540 });
541
542// For the benefit of early-adopters. Can later be removed
543prog
544 .command('adapt')
545 .describe('Customise your production build for different platforms')
546 .option('--verbose', 'Log more stuff', false)
547 .action(async () => {
548 console.log('"svelte-kit build" will now run the adapter');
549 });
550
551prog.parse(process.argv, { unknown: (arg) => `Unknown option: ${arg}` });
552
553export { $ };