diff --git a/cli/package-lock.json b/cli/package-lock.json index f6824f31..852d2fc0 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,18 +1,19 @@ { "name": "elm-land", - "version": "0.14.1", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "elm-land", - "version": "0.14.1", + "version": "0.15.0", "license": "ISC", "dependencies": { "chokidar": "3.5.3", "elm": "0.19.1-5", "elm-esm": "1.1.4", "node-elm-compiler": "5.0.6", + "terser": "5.14.0", "vite": "2.9.8" }, "bin": { @@ -22,6 +23,69 @@ "bats": "1.7.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -133,6 +197,11 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -175,6 +244,11 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1221,6 +1295,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", @@ -1229,6 +1311,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -1276,6 +1367,23 @@ "node": ">=6.0.0" } }, + "node_modules/terser": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1402,6 +1510,54 @@ } }, "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==" + }, + "@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==" + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1491,6 +1647,11 @@ "fill-range": "^7.0.1" } }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1519,6 +1680,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2186,11 +2352,25 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -2221,6 +2401,17 @@ "rimraf": "~2.6.2" } }, + "terser": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/cli/package.json b/cli/package.json index f21156a0..ecc1a6c7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "elm-land", - "version": "0.14.1", + "version": "0.15.0", "description": "Reliable web apps for everyone", "main": "index.js", "bin": { @@ -39,6 +39,7 @@ "elm": "0.19.1-5", "elm-esm": "1.1.4", "node-elm-compiler": "5.0.6", + "terser": "5.14.0", "vite": "2.9.8" } } diff --git a/cli/src/cli.js b/cli/src/cli.js index 7f261e5b..8a2cefe3 100644 --- a/cli/src/cli.js +++ b/cli/src/cli.js @@ -1,6 +1,7 @@ const { Init } = require('./commands/init') const { Add } = require('./commands/add') const { Server } = require('./commands/server') +const { Build } = require('./commands/build') const { Utils } = require('./commands/_utils') let { version } = require('../package.json') @@ -10,8 +11,9 @@ let subcommandList = [ 'Here are the commands:', '✨ elm-land init ...... create a new project', '🚀 elm-land server ................ run a local dev server', + '📦 elm-land build .......... build your app for production', '📄 elm-land add page ................ add a new page', - '🪆 elm-land add layout ........... add a new layout', + '🪆 elm-land add layout ........... add a new layout', '' ] @@ -30,6 +32,9 @@ let run = async (commandFromCli) => { 'server': (args) => { return Server.run({}) }, + 'build': (args) => { + return Build.run({}) + }, 'add': (args) => { return Add.run({ arguments: args }) }, diff --git a/cli/src/commands/build.js b/cli/src/commands/build.js new file mode 100644 index 00000000..70865681 --- /dev/null +++ b/cli/src/commands/build.js @@ -0,0 +1,34 @@ +const { Files } = require("../files") +const { Utils } = require("./_utils") + +let run = async () => { + + let rawTextConfig = undefined + try { + rawTextConfig = await Files.readFromUserFolder('elm-land.json') + } catch (_) { + return Promise.reject(Utils.notInElmLandProject) + } + + let config = {} + try { + config = JSON.parse(rawTextConfig) + } catch (err) { + // TODO: Warn user about invalid config JSON + } + + return { + message: `🌈 Build was successful!`, + files: [], + effects: [ + { kind: 'generateHtml', config }, + { kind: 'build', config } + ] + } +} + +module.exports = { + Build: { + run + } +} \ No newline at end of file diff --git a/cli/src/commands/server.js b/cli/src/commands/server.js index 751f04be..e8600d06 100644 --- a/cli/src/commands/server.js +++ b/cli/src/commands/server.js @@ -18,7 +18,7 @@ let run = async () => { } return { - message: '🌈 Server ready at http://localhost:1234', + message: ({ port }) => `🌈 Server ready at http://localhost:${port}`, files: [], effects: [ { kind: 'generateHtml', config }, diff --git a/cli/src/effects.js b/cli/src/effects.js index 11e88a26..638cbae6 100644 --- a/cli/src/effects.js +++ b/cli/src/effects.js @@ -5,6 +5,10 @@ const ElmVitePlugin = require('./vite-plugin/index.js') const { Codegen } = require('./codegen') const { Files } = require('./files') + +let srcPagesFolderFilepath = path.join(process.cwd(), 'src', 'Pages') +let srcLayoutsFolderFilepath = path.join(process.cwd(), 'src', 'Layouts') + let runServer = async (options) => { try { // Check if `.elm-land` folder exists @@ -42,58 +46,26 @@ let runServer = async (options) => { }) configFileWatcher.on('change', async () => { - let config = {} try { let rawConfig = await Files.readFromUserFolder('elm-land.json') - config = JSON.parse(rawConfig) + let config = JSON.parse(rawConfig) + let result = await generateHtml(config) + if (result.problem) { + console.info(result.problem) + } } catch (_) { } - let result = await generateHtml(config) - if (result.problem) { - console.info(result.problem) - } }) // Listen for changes to src/Pages and src/Layouts folders - let srcPagesFolderFilepath = path.join(process.cwd(), 'src', 'Pages') - let srcLayoutsFolderFilepath = path.join(process.cwd(), 'src', 'Layouts') let srcPagesAndLayoutsFolderWatcher = chokidar.watch([srcPagesFolderFilepath, srcLayoutsFolderFilepath], { ignorePermissionErrors: true, ignoreInitial: true }) - let onPageFileChanged = async () => { - try { - let pageFilepaths = Files.listElmFilepathsInFolder(srcPagesFolderFilepath) - let layouts = Files.listElmFilepathsInFolder(srcLayoutsFolderFilepath).map(filepath => filepath.split('/')) - - let pages = - await Promise.all(pageFilepaths.map(async filepath => { - let contents = await Files.readFromUserFolder(`src/Pages/${filepath}.elm`) - - return { - filepath: filepath.split('/'), - contents - } - })) - - let newFiles = await Codegen.generateElmLandFiles({ pages, layouts }) - - await Files.create( - newFiles.map(generatedFile => ({ - kind: 'file', - name: `.elm-land/src/${generatedFile.filepath}`, - content: generatedFile.contents - })) - ) - - } catch (err) { - console.error(err) - } - } - srcPagesAndLayoutsFolderWatcher.on('all', onPageFileChanged) - await onPageFileChanged() + srcPagesAndLayoutsFolderWatcher.on('all', generateElmFiles) + await generateElmFiles() // Run the vite server on options.port const server = await Vite.createServer({ @@ -113,9 +85,11 @@ let runServer = async (options) => { logLevel: 'silent' }) + + await server.listen() - return { problem: null } + return { problem: null, port: server.httpServer.address().port } } catch (e) { console.error(e) console.log('') @@ -124,6 +98,36 @@ let runServer = async (options) => { } +let generateElmFiles = async () => { + try { + let pageFilepaths = Files.listElmFilepathsInFolder(srcPagesFolderFilepath) + let layouts = Files.listElmFilepathsInFolder(srcLayoutsFolderFilepath).map(filepath => filepath.split('/')) + + let pages = + await Promise.all(pageFilepaths.map(async filepath => { + let contents = await Files.readFromUserFolder(`src/Pages/${filepath}.elm`) + + return { + filepath: filepath.split('/'), + contents + } + })) + + let newFiles = await Codegen.generateElmLandFiles({ pages, layouts }) + + await Files.create( + newFiles.map(generatedFile => ({ + kind: 'file', + name: `.elm-land/src/${generatedFile.filepath}`, + content: generatedFile.contents + })) + ) + + } catch (err) { + console.error(err) + } +} + let attemptToLoadEnvVariablesFromUserConfig = () => { try { let config = require(path.join(process.cwd(), 'elm-land.json')) @@ -147,6 +151,42 @@ const attempt = (fn) => { } } +const build = async (config) => { + // Make sure initial files are up-to-date + await Files.copyPaste({ + source: path.join(__dirname, 'templates', '_elm-land', 'server'), + destination: path.join(process.cwd(), '.elm-land'), + }) + await Files.copyPaste({ + source: path.join(__dirname, 'templates', '_elm-land', 'src'), + destination: path.join(process.cwd(), '.elm-land'), + }) + + // Load ENV variables + attemptToLoadEnvVariablesFromUserConfig() + + // Generate Elm files + await generateElmFiles() + + // Build app in dist folder + await Vite.build({ + configFile: false, + root: path.join(process.cwd(), '.elm-land', 'server'), + publicDir: path.join(process.cwd(), 'static'), + build: { + outDir: '../../dist' + }, + plugins: [ + ElmVitePlugin.plugin({ + debug: false, + optimize: true + }) + ], + logLevel: 'silent' + }) + + return { problem: null } +} // Generating index.html from elm-land.json file const generateHtml = async (config) => { @@ -241,15 +281,21 @@ const generateHtml = async (config) => { let run = async (effects) => { // 1. Perform all effects, one at a time let results = [] + let port = process.env.PORT || 1234 for (let effect of effects) { switch (effect.kind) { case 'runServer': - results.push(await runServer(effect.options)) + let result = await runServer(effect.options) + port = result.port + results.push(result) break case 'generateHtml': results.push(await generateHtml(effect.config)) break + case 'build': + results.push(await build(effect.config)) + break default: results.push({ problem: `❗️ Unrecognized effect: ${effect.kind}` }) break @@ -264,7 +310,7 @@ let run = async (effects) => { } // 3. If there weren't any problems, great! - return { problem: null } + return { problem: null, port } } module.exports = { diff --git a/cli/src/index.js b/cli/src/index.js index 9a8f966e..5edffce3 100755 --- a/cli/src/index.js +++ b/cli/src/index.js @@ -8,9 +8,13 @@ let main = async () => { let output = await Cli.run(process.argv) await Files.create(output.files) - await Effects.run(output.effects) + let data = await Effects.run(output.effects) - console.log(output.message) + if (typeof output.message === 'string') { + console.log(output.message) + } else { + console.log(output.message(data)) + } } catch (err) { console.error(err) process.exit(1) diff --git a/cli/src/templates/_gitignore b/cli/src/templates/_gitignore index 8acb586e..9720e5db 100644 --- a/cli/src/templates/_gitignore +++ b/cli/src/templates/_gitignore @@ -1,3 +1,4 @@ +/dist /.elm-land /elm-stuff /node_modules diff --git a/cli/src/vite-plugin/index.js b/cli/src/vite-plugin/index.js index a8ba3f4c..fff92dae 100644 --- a/cli/src/vite-plugin/index.js +++ b/cli/src/vite-plugin/index.js @@ -4,6 +4,7 @@ const { relative } = require('path') const { injectHMR } = require('./hmrInjector') const { acquireLock } = require('./mutex') const { default: ElmErrorJson } = require('./elm-error-json.js') +const terser = require('terser') const trimDebugMessage = (code) => code.replace(/(console\.warn\('Compiled in DEBUG mode)/, '// $1') const viteProjectPath = (dependency) => `/${relative(process.cwd(), dependency)}` @@ -101,7 +102,7 @@ const plugin = (opts) => { const compiled = await compiler.compileToString(targets, { output: '.js', optimize: typeof optimize === 'boolean' ? optimize : !debug && isBuild, - verbose: isBuild, + verbose: false, debug: typeof debug === 'boolean' ? debug : !isBuild, report: 'json' }) @@ -114,11 +115,29 @@ const plugin = (opts) => { } lastErrorSent = null - server.ws.send('elm:success', { msg: 'Success!' }) + if (server) { + server.ws.send('elm:success', { msg: 'Success!' }) + } + + let minify = async (unminifiedJs) => { + // --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' }) + const { code: step1 } = await terser.minify(unminifiedJs, { compress: { pure_funcs: 'F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9'.split(','), pure_getters: true, keep_fargs: false, unsafe_comps: true, unsafe: true } }) + // --mangle + const { code: step2 } = await terser.minify(step1, { mangle: true }) + return step2 + } - return { - code: isBuild ? esm : trimDebugMessage(injectHMR(esm, dependencies.map(viteProjectPath))), - map: null, + if (isBuild) { + let code = await minify(esm) + return { + code, + map: null + } + } else { + return { + code: trimDebugMessage(injectHMR(esm, dependencies.map(viteProjectPath))), + map: null, + } } } catch (e) { if (e instanceof Error && e.message.includes('-- NO MAIN')) { @@ -128,7 +147,16 @@ const plugin = (opts) => { throw message } else { if (isBuild) { - throw e + try { + let output = ElmErrorJson.parse(e.message) + console.error(`❗️ Elm Land build failed:`) + console.error('') + console.error(ElmErrorJson.toColoredTerminalOutput(output)) + console.error('') + return process.exit(1) + } catch (e) { + throw e + } } else { let elmError = ElmErrorJson.parse(e.message) lastErrorSent = elmError @@ -136,7 +164,6 @@ const plugin = (opts) => { error: ElmErrorJson.toColoredHtmlOutput(elmError) }) - return { code: `export const Elm = new Proxy({}, () => ({ init: () => {} }))`, map: null diff --git a/cli/tests/06-build.bats b/cli/tests/06-build.bats new file mode 100644 index 00000000..0d4d3193 --- /dev/null +++ b/cli/tests/06-build.bats @@ -0,0 +1,16 @@ + +load helpers + +@test "'elm-land build' works with hello world example" { + cd ../examples/01-hello-world + run elm-land build + expectToPass + + expectOutputContains "🌈 Build was successful!" + + cd ../../cli +} + +@test "cleanup" { + cleanupTmpFolder +} \ No newline at end of file diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index 90213a1a..09b02b09 100644 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -62,6 +62,7 @@ module.exports = { '/guide/css', '/guide/state-management', '/guide/data-fetching', + '/guide/deploying', ] } ] diff --git a/docs/src/guide/deploying.md b/docs/src/guide/deploying.md new file mode 100644 index 00000000..b8c637e7 --- /dev/null +++ b/docs/src/guide/deploying.md @@ -0,0 +1,73 @@ +# Deploying to Production + +## Building your app + +When you are ready to publish your Elm Land app, you can use the `elm-land build` command. The build command will handle building, optimizing, and minifying your app for production. + +```sh +elm-land build +``` + + + + +```txt +🌈 Build was successful! +``` + + + + +If the Elm compiler detects _any_ problems, they will be reported as friendly messages in your terminal. + +### Understanding the output + +All Elm Land apps are compiled as "single-page applications" in the `dist` folder. This means no matter what page is requested, that request will need to be directed to a single file: `dist/index.html`. + +Depending on your hosting provider, you may need to add some configuration to tell it to redirect all URL requests to the `dist/index.html` file. + + +## Deploying with Netlify + +In this guide, we'll show you how to deploy your app for free on [Netlify](https://netlify.app/). Netlify is a popular choice for static website and single-page application hosting for frontend projects. + + +### Step 1. The configuration file + +With Netlify, you can add a configuration file to tell Netlify how to build your application, and where those files will be after the build succeeds. + +Add this `netlify.toml`, alongside your `elm-land.json` file, at the root of your project: + + + + +```toml +# 1️⃣ Tells Netlify how to build your app, and where the files are +[build] + command = "npx elm-land build" + publish = "dist" + +# 2️⃣ Handles SPA redirects so all your pages work +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 +``` + + + + +### Step 2. Deploy your site + +In your web browser, you can connect a GitHub repo to your Netlify app here: +[https://app.netlify.com/start](https://app.netlify.com/start) + +![Netlify's "import an existing project" screen in the browser](./deploying/netlify-step-1.png) + +If your Elm Land project is already hosted on [GitHub](https://github.com/), follow the step-by-step process on Netlify to connect that repo to Netlify. + +### Continuous deployment + +Once GitHub and Netlify are connected, anytime you make a commit to the main branch of your repo your changes will automatically be deployed. If you are making something you are excited about, be sure to share it with us on Twitter at [@ElmLand_](https://twitter.com/elmland_) + +We love to see the awesome stuff you build with Elm Land! :heart: \ No newline at end of file diff --git a/docs/src/guide/deploying/netlify-step-1.png b/docs/src/guide/deploying/netlify-step-1.png new file mode 100644 index 00000000..5b1cd8ed Binary files /dev/null and b/docs/src/guide/deploying/netlify-step-1.png differ diff --git a/examples/01-hello-world/.gitignore b/examples/01-hello-world/.gitignore new file mode 100644 index 00000000..17b09e31 --- /dev/null +++ b/examples/01-hello-world/.gitignore @@ -0,0 +1,6 @@ +/.elm-land +/elm-stuff +/node_modules +/dist +.DS_Store +*.pem \ No newline at end of file diff --git a/examples/01-hello-world/_elm-land/server/main.js b/examples/01-hello-world/_elm-land/server/main.js new file mode 100644 index 00000000..59181525 --- /dev/null +++ b/examples/01-hello-world/_elm-land/server/main.js @@ -0,0 +1,106 @@ +import { Elm } from '../src/Main.elm' + +// client side +if (import.meta.hot) { + class ElmErrorOverlay extends HTMLElement { + constructor() { + super() + this.attachShadow({ mode: 'open' }) + } + + onContentChanged(html) { + this.shadowRoot.querySelector('.elm-error').innerHTML = html + } + + connectedCallback() { + this.shadowRoot.innerHTML = ` + +
+
+ ` + } + } + + import.meta.hot.on('elm:error', (data) => { + + if (!customElements.get('elm-error-overlay')) { + customElements.define('elm-error-overlay', ElmErrorOverlay) + } + + let existingOverlay = document.querySelector('elm-error-overlay') + + if (existingOverlay) { + existingOverlay.onContentChanged(data.error) + } else { + document.body.innerHTML += '' + document.querySelector('elm-error-overlay').onContentChanged(data.error) + } + + }) + + import.meta.hot.on('elm:success', () => { + let existingOverlay = document.querySelector('elm-error-overlay') + if (existingOverlay) { + existingOverlay.remove() + } + }) + +} + +let startApp = ({ Interop }) => { + let env = {} + + let flags = Interop.flags + ? Interop.flags({ env }) + : undefined + + try { + let app = Elm.Main.init({ + node: document.getElementById('app'), + flags + }) + + if (Interop.onReady) { + Interop.onReady({ app, env }) + } + } catch (_) { + + } +} + +// If user has defined an interop.js file, use it +try { + let Interop = import.meta.globEager('../../src/interop.js')['../../src/interop.js'] || {} + startApp({ Interop }) +} catch (_) { + startApp({ Interop: {} }) +} \ No newline at end of file diff --git a/examples/01-hello-world/elm-land.json b/examples/01-hello-world/elm-land.json new file mode 100644 index 00000000..2045f0c9 --- /dev/null +++ b/examples/01-hello-world/elm-land.json @@ -0,0 +1,19 @@ +{ + "app": { + "env": [], + "html": { + "attributes": { + "html": { "lang": "en" }, + "head": {}, + "body": {} + }, + "title": "My Elm Land App", + "meta": [ + { "charset": "UTF-8" }, + { "http-equiv": "X-UA-Compatible", "content": "IE=edge" }, + { "name": "viewport", "content": "width=device-width, initial-scale=1.0" } + ], + "link": [] + } + } +} \ No newline at end of file diff --git a/examples/01-hello-world/elm.json b/examples/01-hello-world/elm.json new file mode 100644 index 00000000..5868b5e1 --- /dev/null +++ b/examples/01-hello-world/elm.json @@ -0,0 +1,25 @@ +{ + "type": "application", + "source-directories": [ + "src", + ".elm-land/src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/json": "1.1.3", + "elm/url": "1.0.0" + }, + "indirect": { + "elm/time": "1.0.0", + "elm/virtual-dom": "1.0.3" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/examples/01-hello-world/src/Pages/Home_.elm b/examples/01-hello-world/src/Pages/Home_.elm new file mode 100644 index 00000000..945a2e32 --- /dev/null +++ b/examples/01-hello-world/src/Pages/Home_.elm @@ -0,0 +1,11 @@ +module Pages.Home_ exposing (page) + +import Html +import View exposing (View) + + +page : View msg +page = + { title = "Homepage" + , body = [ Html.text "Hello, world!" ] + }