├── .gitignore
├── src
├── node
│ ├── init
│ │ ├── template
│ │ │ ├── package.json
│ │ │ ├── App.svelte
│ │ │ └── index.html
│ │ └── index.ts
│ ├── serve
│ │ ├── hmrPlugin.ts
│ │ ├── servePlugin.ts
│ │ ├── htmlPlugin.ts
│ │ ├── wss.ts
│ │ ├── sveltePlugin.ts
│ │ ├── index.ts
│ │ └── modulePlugin.ts
│ ├── preprocessors
│ │ ├── utils
│ │ │ ├── importAny.ts
│ │ │ ├── or.ts
│ │ │ ├── resolvedFrom.ts
│ │ │ ├── loadLib.ts
│ │ │ └── installHelper.ts
│ │ ├── types.ts
│ │ ├── transformers
│ │ │ ├── coffeescript.ts
│ │ │ ├── less.ts
│ │ │ ├── sass.ts
│ │ │ ├── typescript.ts
│ │ │ └── stylus.ts
│ │ └── index.ts
│ ├── utils
│ │ └── execute.ts
│ └── tsconfig.json
└── client
│ ├── tsconfig.json
│ └── client.ts
├── bin
└── index.js
├── README.md
├── LICENSE
├── package.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/src/node/init/template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "%%packageName%%",
3 | "version": "1.0.0",
4 | "description": "%%description%%"
5 | }
6 |
--------------------------------------------------------------------------------
/src/node/init/template/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
9 |
Hello {name}
10 |
--------------------------------------------------------------------------------
/src/node/init/template/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/node/serve/hmrPlugin.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import path from "path";
3 |
4 | export default function ({ app }: { app: Koa; root: string }) {
5 | app.use(async (ctx, next) => {
6 | if (ctx.path === "/@hmr") {
7 | ctx.resolvedPath = path.join(__dirname, "../client.js");
8 | }
9 | return next();
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/node/preprocessors/utils/importAny.ts:
--------------------------------------------------------------------------------
1 | import { resolveFrom } from "./resolvedFrom";
2 |
3 | export async function importAny(root: string, modules: string[]) {
4 | for (const mod of modules) {
5 | const resolvedPath = resolveFrom(root, mod);
6 | if (resolvedPath) {
7 | return await import(resolvedPath);
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const args = process.argv.slice(2);
4 |
5 | const command = args[0];
6 | if (command === "init") {
7 | const { initSvelteApp } = require("../dist/init/index.js");
8 | initSvelteApp(args[1]);
9 | } else {
10 | const { startDevServer } = require("../dist/serve/index.js");
11 | startDevServer(args[0] || ".");
12 | }
13 |
--------------------------------------------------------------------------------
/src/node/preprocessors/types.ts:
--------------------------------------------------------------------------------
1 | export interface TransformConfig {
2 | from: string;
3 | to: string;
4 | desc: string;
5 | content: string;
6 | filename: string;
7 | root: string;
8 | options?: { [key: string]: any };
9 | }
10 | export interface TransformResult {
11 | code: string;
12 | map?: object | string;
13 | dependencies?: string[];
14 | }
15 |
--------------------------------------------------------------------------------
/src/node/preprocessors/utils/or.ts:
--------------------------------------------------------------------------------
1 | export function or(modules: string[]) {
2 | if (!Array.isArray(modules)) {
3 | return modules;
4 | }
5 | if (modules.length <= 2) {
6 | return modules.map((mod) => mod).join(" or ");
7 | }
8 | return `${modules
9 | .slice(0, -1)
10 | .map((mod) => mod)
11 | .join(", ")} or ${modules[modules.length - 1]}`;
12 | }
13 |
--------------------------------------------------------------------------------
/src/node/utils/execute.ts:
--------------------------------------------------------------------------------
1 | import { exec, execSync } from "child_process";
2 | import { promisify } from "util";
3 |
4 | const execAsync = promisify(exec);
5 |
6 | export function executeSync(cwd: string, command: string) {
7 | return execSync(command, { cwd, encoding: "utf8", stdio: "pipe" });
8 | }
9 |
10 | export function executeAsync(cwd: string, command: string) {
11 | return execAsync(command, { cwd, encoding: "utf8" });
12 | }
13 |
--------------------------------------------------------------------------------
/src/node/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "../../dist",
4 | "sourceMap": false,
5 | "target": "es2018",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "declaration": true,
9 | "allowJs": false,
10 | "allowSyntheticDefaultImports": true,
11 | "noUnusedLocals": true,
12 | "strictNullChecks": true,
13 | "noImplicitAny": true,
14 | "removeComments": false,
15 | "module": "commonjs",
16 | "lib": ["es2018"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "../../dist",
4 | "sourceMap": false,
5 | "target": "es2018",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "declaration": true,
9 | "allowJs": false,
10 | "allowSyntheticDefaultImports": true,
11 | "noUnusedLocals": true,
12 | "strictNullChecks": true,
13 | "noImplicitAny": true,
14 | "removeComments": false,
15 | "module": "commonjs",
16 | "lib": ["es2018", "DOM"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/node/preprocessors/utils/resolvedFrom.ts:
--------------------------------------------------------------------------------
1 | import { executeSync } from "../../utils/execute";
2 |
3 | export function resolveFrom(fromDirectory: string, moduleId: string) {
4 | try {
5 | // require.resolve has cache that has no API to clear them
6 | // once it failed to resolve, it will always fail to resolve
7 | // this HACKERY allow us to install deps and resolve them again
8 | return executeSync(fromDirectory, `node -e "console.log(require.resolve('${moduleId}'))"`).trim();
9 | } catch {
10 | return undefined;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/node/serve/servePlugin.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import send from "koa-send";
3 |
4 | export default function ({ app, root }: { app: Koa; root: string }) {
5 | app.use(require("koa-conditional-get")());
6 | app.use(require("koa-etag")());
7 | app.use((ctx) => {
8 | if (ctx.resolvedPath) {
9 | return send(ctx, ctx.resolvedPath, {
10 | root: "/",
11 | index: "index.html",
12 | // when installed in global, path will contain hidden files
13 | hidden: true,
14 | });
15 | }
16 | ctx.status = 404;
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # svelte-serve
2 |
3 | Create Svelte app with no configuration.
4 |
5 | ## Quick Overview
6 |
7 | ### Init a Svelte app
8 |
9 | ```sh
10 | npx svelte-serve init app-name
11 | ```
12 |
13 | ### Init a Svelte app from [Svelte REPL](https://svelte.dev/repl)
14 |
15 | ```sh
16 | npx svelte-serve init https://svelte.dev/repl/hello-world?version=3.22.2
17 | ```
18 |
19 | ### Serve Svelte app from a folder
20 |
21 | ```sh
22 | npx svelte-serve .
23 | ```
24 |
25 | ## TODO
26 | - [ ] preprocessors (in progress)
27 | - [ ] HMR
28 | - [ ] source map
29 |
30 | ## License
31 |
32 | MIT
--------------------------------------------------------------------------------
/src/node/preprocessors/utils/loadLib.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 | import { importAny } from "./importAny";
3 | import { promptToInstall } from "./installHelper";
4 |
5 | export async function loadLib(libs: string[], { errorMessage, root }: { errorMessage: string; root: string }) {
6 | let lib = await importAny(root, libs);
7 | if (lib) return lib;
8 |
9 | console.error(chalk.red(errorMessage.replace("$1", libs.map((lib) => `"${lib}"`).join(", "))));
10 |
11 | // attempt to install
12 | if (await promptToInstall(root, [libs])) {
13 | return await importAny(root, libs);
14 | }
15 |
16 | throw new Error("failed to load lib");
17 | }
18 |
--------------------------------------------------------------------------------
/src/node/serve/htmlPlugin.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import getStream from "get-stream";
3 | // import path from 'path';
4 | // import resolve from 'resolve-from';
5 | // import chalk from 'chalk';
6 |
7 | export default function ({ app, root }: { app: Koa; root: string }) {
8 | app.use(async (ctx, next) => {
9 | await next();
10 |
11 | if (ctx.status === 304) {
12 | // Not modified
13 | return;
14 | }
15 |
16 | if (ctx.response.header["content-type"]?.indexOf("text/html") > -1) {
17 | const body = await getStream(ctx.body);
18 | ctx.body = '' + body;
19 | }
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/node/preprocessors/transformers/coffeescript.ts:
--------------------------------------------------------------------------------
1 | import type { TransformConfig } from "../types";
2 | import { loadLib } from "../utils/loadLib";
3 | import { resolveFrom } from "../utils/resolvedFrom";
4 |
5 | let coffeeScript: any;
6 |
7 | export function getMissingDependencies({ root }: TransformConfig) {
8 | if (!resolveFrom(root, "coffeescript")) {
9 | return ["coffeescript"];
10 | }
11 | }
12 |
13 | export default async function ({ to, desc, root, content, filename, options = {} }: TransformConfig) {
14 | if (!coffeeScript) {
15 | const coffeeLib = await loadLib(["coffeescript"], {
16 | errorMessage: `$1 are required for <${to} ${desc}>`,
17 | root,
18 | });
19 | coffeeScript = coffeeLib.default;
20 | }
21 |
22 | const { js: code, sourceMap: map } = coffeeScript.compile(content, {
23 | filename,
24 | sourceMap: true,
25 | bare: false,
26 | ...options,
27 | });
28 |
29 | return {
30 | code,
31 | map,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/node/preprocessors/transformers/less.ts:
--------------------------------------------------------------------------------
1 | import type { TransformConfig } from "../types";
2 | import path from "path";
3 | import { loadLib } from "../utils/loadLib";
4 | import { resolveFrom } from "../utils/resolvedFrom";
5 |
6 | let less: any;
7 |
8 | export function getMissingDependencies({ root }: TransformConfig) {
9 | if (!resolveFrom(root, "less")) {
10 | return ["less"];
11 | }
12 | }
13 |
14 | export default async function ({ to, desc, root, content, filename, options = {} }: TransformConfig) {
15 | if (!less) {
16 | const lessLib = await loadLib(["less"], {
17 | errorMessage: `$1 are required for <${to} ${desc}>`,
18 | root,
19 | });
20 | less = lessLib.default;
21 | }
22 |
23 | const { css: code, map, imports: dependencies } = await less.render(content, {
24 | sourceMap: {},
25 | filename,
26 | paths: [...(options.includePaths || []), "node_modules", path.dirname(filename)],
27 | ...options,
28 | });
29 | return {
30 | code,
31 | map,
32 | dependencies,
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2020 Tan Li Hau
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/src/node/preprocessors/transformers/sass.ts:
--------------------------------------------------------------------------------
1 | import { renderSync } from "sass";
2 | import type { TransformConfig } from "../types";
3 | import path from "path";
4 | import { loadLib } from "../utils/loadLib";
5 | import { resolveFrom } from "../utils/resolvedFrom";
6 |
7 | let sass: { renderSync: typeof renderSync };
8 |
9 | export function getMissingDependencies({ root }: TransformConfig) {
10 | if (!(resolveFrom(root, "sass") || resolveFrom(root, "node-sass"))) {
11 | return ["sass", "node-sass"];
12 | }
13 | }
14 |
15 | export default async function ({ to, desc, root, content, filename, options = {} }: TransformConfig) {
16 | if (!sass) {
17 | const sassLib = await loadLib(["sass", "node-sass"], {
18 | errorMessage: `$1 are required for <${to} ${desc}>`,
19 | root,
20 | });
21 | sass = sassLib.default;
22 | }
23 |
24 | const result = sass.renderSync({
25 | sourceMap: true,
26 | ...options,
27 | data: content,
28 | includePaths: [...(options.includePaths || []), "node_modules", path.dirname(filename)],
29 | outFile: `${filename}.css`,
30 | });
31 | return {
32 | code: result.css.toString(),
33 | map: result.map?.toString(),
34 | dependencies: result.stats.includedFiles,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/node/preprocessors/transformers/typescript.ts:
--------------------------------------------------------------------------------
1 | import type { TransformConfig } from "../types";
2 | import { loadLib } from "../utils/loadLib";
3 | import { resolveFrom } from "../utils/resolvedFrom";
4 |
5 | let ts: any;
6 |
7 | export function getMissingDependencies({ root }: TransformConfig) {
8 | if (!resolveFrom(root, "typescript")) {
9 | return ["typescript"];
10 | }
11 | }
12 |
13 | export default async function ({ to, desc, root, content, filename, options = {} }: TransformConfig) {
14 | if (!ts) {
15 | const tsLib = await loadLib(["typescript"], {
16 | errorMessage: `$1 is required for <${to} ${desc}>`,
17 | root,
18 | });
19 | ts = tsLib.default;
20 | }
21 |
22 | const compilerOptions = {
23 | ...options,
24 | moduleResolution: "node",
25 | target: "es6",
26 | allowNonTsExtensions: true,
27 | };
28 |
29 | const { outputText: code, sourceMapText: map, diagnostics } = ts.transpileModule(content, {
30 | fileName: filename,
31 | compilerOptions: compilerOptions,
32 | // reportDiagnostics: true, //options.reportDiagnostics !== false,
33 | // transformers: TS_TRANSFORMERS,
34 | });
35 |
36 | // TODO:
37 | diagnostics;
38 |
39 | return {
40 | code,
41 | map,
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/node/serve/wss.ts:
--------------------------------------------------------------------------------
1 | import WebSocket from "ws";
2 | import { Server } from "http";
3 | import chalk from "chalk";
4 |
5 | let wss;
6 | let id = 0;
7 | const sockets = new Map();
8 |
9 | export function initialiseWebSocketServer(server: Server) {
10 | wss = new WebSocket.Server({ server });
11 | wss.on("connection", (socket) => {
12 | let _id = id++;
13 | sockets.set(_id, socket);
14 | socket.send(JSON.stringify({ type: "connected", id: _id }));
15 | socket.on("close", () => {
16 | sockets.delete(_id);
17 | });
18 | });
19 |
20 | wss.on("error", (e: Error & { code: string }) => {
21 | if (e.code !== "EADDRINUSE") {
22 | console.error(chalk.red(`WebSocket server error:`));
23 | console.error(e);
24 | }
25 | });
26 | }
27 |
28 | export function broadcast(message: any) {
29 | const stringified = JSON.stringify(message, null, 2);
30 | // console.log(`broadcast message: ${stringified}`);
31 |
32 | sockets.forEach((s) => s.send(stringified));
33 | }
34 |
35 | export function send(socketId: number, message: any) {
36 | const stringified = JSON.stringify(message, null, 2);
37 | // console.log(`send message to ${socketId}: ${stringified}`);
38 | // console.group(sockets);
39 | sockets.get(socketId)?.send(stringified);
40 | }
41 |
--------------------------------------------------------------------------------
/src/node/serve/sveltePlugin.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import path from "path";
3 | import { compile, preprocess } from "svelte/compiler";
4 | import getStream from "get-stream";
5 | import { getMissingDependenciesPreprocessor, getTransformCodePreprocessor } from "../preprocessors";
6 |
7 | export default function ({ app, root }: { app: Koa; root: string }) {
8 | app.use(async (ctx, next) => {
9 | if (!ctx.resolvedPath) return next();
10 | if (!ctx.resolvedPath.endsWith(".svelte")) return next();
11 |
12 | await next();
13 |
14 | if (ctx.status === 304) {
15 | // Not modified
16 | return;
17 | }
18 |
19 | const svelteCode = await getStream(ctx.body);
20 |
21 | const { code: preprocessedCode, dependencies } = await preprocess(
22 | svelteCode,
23 | [getMissingDependenciesPreprocessor(root), getTransformCodePreprocessor(root)],
24 | {
25 | filename: path.basename(ctx.path),
26 | }
27 | );
28 | const { js } = compile(preprocessedCode, {});
29 |
30 | // TODO: watch these files too
31 | dependencies;
32 |
33 | // set sourcemap
34 | // cache.set(req.url + '.map', js.map);
35 | // jsCode = jsCode + `\n\n//# sourceMappingURL=${req.url}.map`;
36 |
37 | ctx.body = js.code;
38 | ctx.set("Content-Type", "text/javascript");
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/node/serve/index.ts:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import Koa from "koa";
3 | import chalk from "chalk";
4 | import path from "path";
5 | import modulePlugin from "./modulePlugin";
6 | import servePlugin from "./servePlugin";
7 | import sveltePlugin from "./sveltePlugin";
8 | import htmlPlugin from "./htmlPlugin";
9 | import hmrPlugin from "./hmrPlugin";
10 | import { initialiseWebSocketServer } from "./wss";
11 |
12 | export function startDevServer(root: string) {
13 | root = path.join(process.cwd(), root);
14 |
15 | const app = new Koa();
16 | modulePlugin({ app, root });
17 | hmrPlugin({ app, root });
18 | sveltePlugin({ app, root });
19 | htmlPlugin({ app, root });
20 | servePlugin({ app, root });
21 |
22 | const server = http.createServer(app.callback());
23 | initialiseWebSocketServer(server);
24 |
25 | let port = 3000;
26 |
27 | server.on("error", (error) => {
28 | // @ts-ignore
29 | if (error.code === "EADDRINUSE") {
30 | setTimeout(() => {
31 | server.close();
32 | server.listen(++port);
33 | }, 100);
34 | } else {
35 | console.log(chalk.red("server error:"));
36 | console.error(error);
37 | }
38 | });
39 | server.on("listening", () => {
40 | console.log(chalk.green("Dev server running at:"));
41 | console.log(` > http://localhost:${port}`);
42 | console.log(" ");
43 | });
44 | server.listen(port);
45 | }
46 |
--------------------------------------------------------------------------------
/src/node/preprocessors/transformers/stylus.ts:
--------------------------------------------------------------------------------
1 | import type { TransformConfig } from "../types";
2 | import path from "path";
3 | import { loadLib } from "../utils/loadLib";
4 | import { resolveFrom } from "../utils/resolvedFrom";
5 |
6 | let stylus: any;
7 |
8 | export function getMissingDependencies({ root }: TransformConfig) {
9 | if (!resolveFrom(root, "stylus")) {
10 | return ["stylus"];
11 | }
12 | }
13 |
14 | export default async function ({ desc, to, root, content, filename, options = {} }: TransformConfig) {
15 | if (!stylus) {
16 | const stylusLib = await loadLib(["stylus"], {
17 | errorMessage: `$1 is required for <${to} ${desc}>`,
18 | root,
19 | });
20 | stylus = stylusLib.default;
21 | }
22 |
23 | return new Promise((resolve, reject) => {
24 | const style = stylus(content, {
25 | filename,
26 | includePaths: [...(options.includePaths || []), "node_modules", path.dirname(filename)],
27 | ...options,
28 | }).set("sourcemap", { ...options.sourcemap });
29 |
30 | style.render((err: Error, css: string) => {
31 | // istanbul ignore next
32 | if (err) reject(err);
33 |
34 | resolve({
35 | code: css,
36 | map: style.sourcemap,
37 | // .map() necessary for windows compatibility
38 | dependencies: style.deps(filename).map((filePath: string) => path.resolve(filePath)),
39 | });
40 | });
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-serve",
3 | "version": "0.1.2",
4 | "license": "MIT",
5 | "bin": {
6 | "svelte-serve": "./bin/index.js"
7 | },
8 | "scripts": {
9 | "dev": "run-p dev:client dev:node",
10 | "dev:client": "tsc -p src/client/tsconfig.json -w",
11 | "dev:node": "tsc -p src/node/tsconfig.json -w",
12 | "build": "tsc -p src/client/tsconfig.json && tsc -p src/node/tsconfig.json && rm -rf dist/init/template && cp -r src/node/init/template dist/init/template"
13 | },
14 | "files": [
15 | "bin",
16 | "dist"
17 | ],
18 | "dependencies": {
19 | "chalk": "^4.0.0",
20 | "es-module-lexer": "^0.3.18",
21 | "get-stream": "^5.1.0",
22 | "inquirer": "^7.1.0",
23 | "koa": "^2.11.0",
24 | "koa-conditional-get": "^2.0.0",
25 | "koa-etag": "^3.0.0",
26 | "koa-send": "^5.0.0",
27 | "koa-static": "^5.0.0",
28 | "lru-cache": "^5.1.1",
29 | "magic-string": "^0.25.7",
30 | "node-fetch": "^2.6.0",
31 | "ora": "^4.0.4",
32 | "resolve-from": "^5.0.0",
33 | "svelte": "^3.21.0",
34 | "svelte-hmr": "^0.7.0",
35 | "ws": "^7.2.5"
36 | },
37 | "devDependencies": {
38 | "@types/es-module-lexer": "^0.3.0",
39 | "@types/estree": "^0.0.44",
40 | "@types/inquirer": "^6.5.0",
41 | "@types/koa": "^2.11.3",
42 | "@types/koa-send": "^4.1.2",
43 | "@types/node-fetch": "^2.5.7",
44 | "@types/sass": "^1.16.0",
45 | "@types/ws": "^7.2.4",
46 | "estree-walker": "^2.0.1",
47 | "npm-run-all": "^4.1.5",
48 | "typescript": "^3.8.3"
49 | },
50 | "prettier": {
51 | "printWidth": 140
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/node/preprocessors/utils/installHelper.ts:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 | import inquirer from "inquirer";
3 | import ora from "ora";
4 | import { executeAsync } from "../../utils/execute";
5 | import { or } from "./or";
6 |
7 | type ModuleOption = string[];
8 | export async function promptToInstall(root: string, modules: ModuleOption[]) {
9 | console.log(`To fix this, you need to install the following module${modules.length > 0 ? "s" : ""}:`);
10 | modules.forEach((mod) => {
11 | console.log("-", or(mod.map((m) => bold(m))));
12 | });
13 | console.log("");
14 | const { install } = await prompt([{ type: "confirm", name: "install", default: true, message: "Do you want to install them now?" }]);
15 | if (install) {
16 | const modsToInstall = [];
17 | for (const mod of modules) {
18 | if (!Array.isArray(mod)) {
19 | modsToInstall.push(mod);
20 | } else if (mod.length === 1) {
21 | modsToInstall.push(mod[0]);
22 | } else {
23 | const { chosen } = await prompt([
24 | { type: "rawlist", name: "chosen", choices: mod, default: 0, message: "Which one would you like to install?" },
25 | ]);
26 | modsToInstall.push(chosen);
27 | }
28 | }
29 |
30 | const spinner = ora(`Installing ${modsToInstall.join(", ")}`).start();
31 | await executeAsync(root, `yarn add --dev ${modsToInstall.join(" ")}`);
32 | spinner.succeed(`Installed ${modsToInstall.join(", ")}`);
33 |
34 | return true;
35 | }
36 | }
37 |
38 | function bold(str: string) {
39 | return chalk.bold('"' + str + '"');
40 | }
41 |
42 | let close: Function;
43 | function prompt(questions: any): any {
44 | // cleanup previous inquirer
45 | if (typeof close === "function") {
46 | close();
47 | }
48 | let promise = inquirer.prompt(questions);
49 | // @ts-ignore
50 | close = promise.ui.close.bind(promise.ui);
51 | return promise;
52 | }
53 |
--------------------------------------------------------------------------------
/src/client/client.ts:
--------------------------------------------------------------------------------
1 | const socket = new WebSocket(`ws://${location.host}`);
2 | socket.addEventListener("message", ({ data }) => {
3 | data = JSON.parse(data);
4 | console.log("received message:", data);
5 |
6 | switch (data.type) {
7 | case "connected":
8 | document.cookie = `__SVELTE_SERVE_ID__=${data.id};`;
9 | break;
10 | case "missing_dependencies":
11 | handleMissingDependencies(data);
12 | break;
13 | case "missing_dependencies_done":
14 | handleMissingDependenciesDone();
15 | break;
16 | }
17 | });
18 | socket.addEventListener("close", () => {
19 | console.log("Connection lost. Polling for restart...");
20 | setInterval(() => {
21 | const socket = new WebSocket(`ws://${location.host}`);
22 | socket.addEventListener("open", () => {
23 | location.reload();
24 | });
25 | }, 1000);
26 | });
27 |
28 | const HMR_MISSING_DEPENDENCIES = "hmr_missing_dependencies";
29 | function handleMissingDependencies({ message, dependencies }: { message: string[]; dependencies: string[][] }) {
30 | const div = document.createElement("div");
31 | div.id = HMR_MISSING_DEPENDENCIES;
32 | div.innerHTML = `
33 |
34 |
Missing dependencies
35 |
Reason:
36 |
37 |
38 | ${message.map((line) => `- ${line.replace(//g, ">").replace(/\n/g, "
")} `).join("")}
39 |
40 |
41 |
Install them in your console:
42 |
yarn add ${dependencies
43 | .map((dep) => dep[0])
44 | .join(" ")}
45 |
46 | `;
47 | document.body.appendChild(div);
48 | }
49 | function handleMissingDependenciesDone() {
50 | const elem = document.querySelector("#" + HMR_MISSING_DEPENDENCIES);
51 | elem?.parentNode?.removeChild(elem);
52 | }
53 |
--------------------------------------------------------------------------------
/src/node/serve/modulePlugin.ts:
--------------------------------------------------------------------------------
1 | import Koa from "koa";
2 | import path from "path";
3 | import resolve from "resolve-from";
4 | import chalk from "chalk";
5 | import getStream from "get-stream";
6 | import MagicString from "magic-string";
7 | import { init, parse } from "es-module-lexer";
8 |
9 | export default function ({ app, root }: { app: Koa; root: string }) {
10 | app.use(async (ctx, next) => {
11 | let resolvedPath;
12 | if (ctx.path.startsWith("/@modules/")) {
13 | const moduleName = ctx.path.replace("/@modules/", "");
14 |
15 | if (!path.extname(moduleName)) {
16 | try {
17 | const pkgPath = resolve(root, moduleName + "/package.json");
18 | const pkg = require(pkgPath);
19 | const entryPoint = pkg.svelte || pkg.module || pkg.main || "index.js";
20 | return ctx.redirect(path.join(ctx.path, entryPoint));
21 | } catch (error) {
22 | if (moduleName === "svelte" || moduleName.startsWith("svelte/")) {
23 | const pkgPath = resolve(__dirname, moduleName + "/package.json");
24 | const pkg = require(pkgPath);
25 | const entryPoint = pkg.svelte || pkg.module || pkg.main || "index.js";
26 | resolvedPath = path.resolve(path.dirname(pkgPath), entryPoint);
27 | } else {
28 | console.log(chalk.red(`Module not found: ${moduleName}`));
29 | }
30 | }
31 | } else {
32 | resolvedPath = resolve(root, moduleName);
33 | }
34 | } else {
35 | resolvedPath = path.resolve(root, "." + ctx.path);
36 | }
37 |
38 | ctx.resolvedPath = resolvedPath;
39 | await next();
40 |
41 | if (ctx.status === 304) {
42 | // Not modified
43 | return;
44 | }
45 |
46 | if (/\.(js|ts|mjs|svelte)$/.test(ctx.path)) {
47 | // console.log(typeof ctx.body);
48 | let jsCode = typeof ctx.body === "string" ? ctx.body : await getStream(ctx.body);
49 | await init;
50 |
51 | // jsCode = jsCode + `\n\n//# sourceMappingURL=${req.url}.map`;
52 | let magicJsCode = new MagicString(jsCode);
53 | const [imports] = parse(jsCode);
54 | for (const { s, e, d } of imports) {
55 | if (d > -1) {
56 | // TODO: dynamic import
57 | }
58 | const importee = jsCode.substring(s, e);
59 | if (!importee.startsWith("./") && !importee.startsWith("../")) {
60 | magicJsCode.overwrite(s, e, "/@modules/" + importee);
61 | }
62 | }
63 | ctx.body = magicJsCode.toString();
64 | }
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/node/init/index.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs";
2 | import chalk from "chalk";
3 | import path from "path";
4 | import inquirer from "inquirer";
5 | import { URLSearchParams } from "url";
6 | import fetch from "node-fetch";
7 | import { init, parse } from "es-module-lexer";
8 | import { executeAsync } from "../utils/execute";
9 | import ora from "ora";
10 |
11 | const SVELTE_REPL = /^https:\/\/svelte.dev\/repl\/([0-9a-f]{32})(\?.*)?$/;
12 |
13 | export async function initSvelteApp(folderName: string | void) {
14 | const cwd = process.cwd();
15 | let templateDirectory = path.join(__dirname, "template");
16 | let targetDirectory: string;
17 |
18 | if (isSvelteRepl(folderName)) {
19 | // @ts-ignore
20 | const [_, replId, queryParams] = folderName.match(SVELTE_REPL);
21 | const query = new URLSearchParams(queryParams);
22 | const { files, name: description } = await getReplContent(replId);
23 | folderName = await askForFolderName(`repl-${replId}`);
24 |
25 | const spinner = ora(`Downloading Svelte REPL`).start();
26 |
27 | targetDirectory = path.join(cwd, folderName);
28 | await fs.mkdir(targetDirectory);
29 |
30 | const dependencies = new Set();
31 | for (const { name, source } of files) {
32 | await fs.writeFile(path.join(targetDirectory, name), source, "utf-8");
33 |
34 | const deps = await getImportStatement(source);
35 | deps.forEach((dep) => dependencies.add(dep));
36 | }
37 | await copyFile("index.html");
38 | await copyFile("package.json", { "%%packageName%%": folderName, "%%description%%": description });
39 |
40 | dependencies.add(`svelte${query.has("version") ? `@${query.get("version")}` : ""}`);
41 |
42 | spinner.text = "Installing dependencies";
43 |
44 | await executeAsync(targetDirectory, `yarn add ${[...dependencies].join(" ")}`);
45 |
46 | spinner.succeed(`Initialised ${folderName} at ${targetDirectory}!`);
47 | printWelcome(folderName);
48 | return;
49 | }
50 |
51 | let shouldCreateFolder = true;
52 | if (!folderName) {
53 | // if the current folder is empty,
54 | // init the svelte app here and dont create a new folder
55 | if ((await fs.readdir(cwd)).length === 0) {
56 | folderName = path.basename(cwd);
57 | targetDirectory = cwd;
58 | shouldCreateFolder = false;
59 | } else {
60 | folderName = await askForFolderName();
61 | targetDirectory = path.join(cwd, folderName);
62 | }
63 | } else {
64 | targetDirectory = path.join(cwd, folderName);
65 | }
66 |
67 | const spinner = ora(`Initialising`).start();
68 |
69 | if (shouldCreateFolder) {
70 | try {
71 | await fs.mkdir(targetDirectory);
72 | } catch (e) {
73 | return spinner.fail(`Target directory "${folderName}" already exists.`);
74 | }
75 | }
76 |
77 | try {
78 | await copyFile("index.html");
79 | await copyFile("App.svelte");
80 | await copyFile("package.json", { "%%packageName%%": folderName, "%%description%%": "svelte-serve template" });
81 |
82 | spinner.succeed(`Initialised ${folderName} at ${targetDirectory}!`);
83 | printWelcome(folderName);
84 | } catch (error) {
85 | return spinner.fail(error.message);
86 | }
87 |
88 | async function copyFile(filename: string, replacement?: Record) {
89 | let content = await fs.readFile(path.join(templateDirectory, filename), "utf-8");
90 | if (replacement) {
91 | for (const key of Object.keys(replacement)) {
92 | const replacementRegex = new RegExp(key, "g");
93 | content = content.replace(replacementRegex, replacement[key]);
94 | }
95 | }
96 | await fs.writeFile(path.join(targetDirectory, filename), content, "utf-8");
97 | }
98 | }
99 |
100 | async function askForFolderName(defaultValue: string | void) {
101 | const { folderName = "svelte-app" } = await inquirer.prompt([
102 | {
103 | type: "input",
104 | name: "folderName",
105 | message: "Please specify folder name:",
106 | default: defaultValue,
107 | validate(str) {
108 | return !!str;
109 | },
110 | },
111 | ]);
112 | return folderName;
113 | }
114 |
115 | function isSvelteRepl(url: string | void) {
116 | return !!url && SVELTE_REPL.test(url);
117 | }
118 |
119 | async function getReplContent(replId: string): Promise<{ files: Array<{ name: string; source: string }>; name: string; uid: string }> {
120 | const response = await fetch(`https://svelte.dev/repl/${replId}.json`);
121 | return await response.json();
122 | }
123 |
124 | async function getImportStatement(source: string) {
125 | await init;
126 |
127 | let dependencies = new Set();
128 | let match;
129 | const regex = /|