diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000000..e94f8140cc --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e99baefce..b3d58e6c90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,11 +85,21 @@ jobs: timeout-minutes: 10 run: | bundle install --jobs 4 --retry 3 --path vendor/bundle + - name: Restore node modules cache + id: yarn-cache + uses: actions/cache@preview + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install yarn + run: yarn install - name: Prepare database run: | bundle exec rake alchemy:spec:prepare - name: Run tests & publish code coverage - uses: paambaati/codeclimate-action@v2.5.5 + uses: paambaati/codeclimate-action@v2.5.6 env: CC_TEST_REPORTER_ID: bca4349e32f97919210ac8a450b04904b90683fcdd57d65a22c0f5065482bc22 with: @@ -99,3 +109,28 @@ jobs: with: name: Screenshots path: spec/dummy/tmp/screenshots + Jest: + runs-on: ubuntu-latest + env: + NODE_ENV: test + steps: + - uses: actions/checkout@v1 + - name: Restore node modules cache + uses: actions/cache@preview + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install yarn + run: yarn install + - name: Run jest + run: yarn jest + - name: Run jest & publish code coverage + uses: paambaati/codeclimate-action@v2.5.6 + env: + CC_TEST_REPORTER_ID: bca4349e32f97919210ac8a450b04904b90683fcdd57d65a22c0f5065482bc22 + with: + coverageLocations: + ./coverage/lcov.info:lcov + coverageCommand: yarn jest --collectCoverage --coverageDirectory=coverage diff --git a/.gitignore b/.gitignore index 3a28ffe484..31145e7fec 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ spec/dummy/public/assets .ruby-version .env .rspec +node_modules +yarn-error.log +yarn-debug.log* +.yarn-integrity +yarn.lock +/public/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..348cb46f7b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "trailingComma": "none", + "vueIndentScriptAndStyle": true, + "arrowParens": "always" +} diff --git a/alchemy_cms.gemspec b/alchemy_cms.gemspec index 7d442a1b3c..f507730719 100644 --- a/alchemy_cms.gemspec +++ b/alchemy_cms.gemspec @@ -41,6 +41,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'simple_form', ['>= 4.0', '< 6'] gem.add_runtime_dependency 'sprockets', ['>= 3.0', '< 5'] gem.add_runtime_dependency 'turbolinks', ['>= 2.5'] + gem.add_runtime_dependency 'webpacker', ['>= 4.0', '< 6'] gem.add_development_dependency 'capybara', ['~> 3.0'] gem.add_development_dependency 'capybara-screenshot', ['~> 1.0'] diff --git a/app/assets/config/alchemy_manifest.js b/app/assets/config/alchemy_manifest.js index 383a389638..e812956c07 100644 --- a/app/assets/config/alchemy_manifest.js +++ b/app/assets/config/alchemy_manifest.js @@ -5,6 +5,7 @@ //= link alchemy/menubar.js //= link alchemy/print.css //= link alchemy/welcome.css +//= link alchemy/favicon.ico //= link tinymce/plugins/alchemy_link/plugin.min.js //= link tinymce/tinymce.min.js //= link_directory ../stylesheets/tinymce/skins/alchemy/ .css diff --git a/app/assets/javascripts/alchemy/admin.js b/app/assets/javascripts/alchemy/admin.js index 7b728dfd24..921c556f26 100644 --- a/app/assets/javascripts/alchemy/admin.js +++ b/app/assets/javascripts/alchemy/admin.js @@ -34,7 +34,6 @@ //= require alchemy/alchemy.growler //= require alchemy/alchemy.gui //= require alchemy/alchemy.hotkeys -//= require alchemy/alchemy.i18n //= require alchemy/alchemy.image_cropper //= require alchemy/alchemy.image_overlay //= require alchemy/alchemy.string_extension @@ -49,6 +48,5 @@ //= require alchemy/alchemy.spinner //= require alchemy/alchemy.tinymce //= require alchemy/alchemy.tooltips -//= require alchemy/alchemy.translations //= require alchemy/alchemy.trash_window //= require alchemy/page_select diff --git a/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee b/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee index 1d86ff16f1..80ae7b18be 100644 --- a/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee +++ b/app/assets/javascripts/alchemy/alchemy.element_editors.js.coffee @@ -179,8 +179,6 @@ Alchemy.ElementEditors = if data.message == 'Alchemy.focusElementEditor' $element = $("#element_#{data.element_id}") Alchemy.ElementEditors.focusElement($element) - else - console.warn 'Unknown message received!', data onClickBody: (e) -> element = $(e.target).parents('.element-editor')[0] diff --git a/app/assets/javascripts/alchemy/alchemy.i18n.js.coffee b/app/assets/javascripts/alchemy/alchemy.i18n.js.coffee deleted file mode 100644 index 5f08c82c07..0000000000 --- a/app/assets/javascripts/alchemy/alchemy.i18n.js.coffee +++ /dev/null @@ -1,32 +0,0 @@ -#= require alchemy/alchemy.translations - -window.Alchemy = {} if typeof(window.Alchemy) is 'undefined' - -Alchemy.I18n = - - KEY_SEPARATOR: /\./ - - # Translates given string - # - translate: (key, replacement) -> - if !Alchemy.locale? - throw 'Alchemy.locale is not set! Please set Alchemy.locale to a locale string in order to translate something.' - translations = Alchemy.translations[Alchemy.locale] - if translations - if @KEY_SEPARATOR.test(key) - keys = key.split(@KEY_SEPARATOR) - translation = translations[keys[0]][keys[1]] || key - else - translation = translations[key] || key - if replacement - translation.replace(/%\{.+\}/, replacement) - else - translation - else - console.warn "Translations for locale #{Alchemy.locale} not found!" - key - -# Global utility method for translating a given string -# -Alchemy.t = (key, replacement) -> - Alchemy.I18n.translate(key, replacement) diff --git a/app/assets/javascripts/alchemy/alchemy.translations.js.coffee b/app/assets/javascripts/alchemy/alchemy.translations.js.coffee deleted file mode 100644 index 57e2f84d49..0000000000 --- a/app/assets/javascripts/alchemy/alchemy.translations.js.coffee +++ /dev/null @@ -1,29 +0,0 @@ -window.Alchemy = {} if typeof(window.Alchemy) is 'undefined' - -# Holds translations for javascripts -# -Alchemy.translations = - en: - allowed_chars: 'of %{count} chars' - cancel: 'Cancel' - cancelled: 'Cancelled' - click_to_edit: 'click to edit' - complete: 'Complete' - element_dirty_notice: 'This element has unsaved changes. Do you really want to fold it?' - help: 'Help' - ok: 'Ok' - page_dirty_notice: 'You have unsaved changes on this page. They will be lost if you continue.' - page_found: 'Page found' - pages_found: 'Pages found' - url_validation_failed: 'The url has no valid format.' - warning: 'Warning!' - 'File is too large': 'File is too large' - 'File is too small': 'File is too small' - 'File type not allowed': 'File type not allowed' - 'Maximum number of files exceeded': 'Maximum number of files exceeded.' - 'Uploaded bytes exceed file size': 'Uploaded bytes exceed file size' - formats: - datetime: "Y-m-d H:i" - date: "Y-m-d" - time: "H:i" - time_24hr: false diff --git a/app/helpers/alchemy/admin/base_helper.rb b/app/helpers/alchemy/admin/base_helper.rb index ca7afd44c0..af2ded90cf 100644 --- a/app/helpers/alchemy/admin/base_helper.rb +++ b/app/helpers/alchemy/admin/base_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "webpacker/helper" + module Alchemy module Admin # This module contains helper methods for rendering dialogs, toolbar buttons and confirmation windows. @@ -14,6 +16,15 @@ module Admin module BaseHelper include Alchemy::BaseHelper include Alchemy::Admin::NavigationHelper + include ::Webpacker::Helper + + def current_webpacker_instance + if controller_name == 'pages' && action_name == 'show' + super + else + Alchemy.webpacker + end + end # Returns a string showing the name of the currently logged in user. # diff --git a/app/javascript/alchemy/admin/__tests__/i18n.spec.js b/app/javascript/alchemy/admin/__tests__/i18n.spec.js new file mode 100644 index 0000000000..0f2ffbbeae --- /dev/null +++ b/app/javascript/alchemy/admin/__tests__/i18n.spec.js @@ -0,0 +1,70 @@ +import translate from "../i18n" + +describe("translate", () => { + describe("if Alchemy.locale is not set", () => { + it("Throws an error", () => { + expect(() => { + translate("help") + }).toThrow("Alchemy.locale is not set") + }) + }) + + describe("if Alchemy.locale is set to a known locale", () => { + beforeEach(() => { + Alchemy.locale = "en" + }) + + describe("if translation is present", () => { + it("Returns translated string", () => { + expect(translate("help")).toEqual("Help") + }) + + describe("if key includes a period", () => { + describe("that is translated", () => { + it("splits into group", () => { + expect(translate("formats.date")).toEqual("Y-m-d") + }) + }) + + describe("that is not translated", () => { + it("returns key", () => { + expect(translate("formats.lala")).toEqual("formats.lala") + }) + }) + + describe("that has unknown group", () => { + it("returns key", () => { + expect(translate("foo.bar")).toEqual("foo.bar") + }) + }) + }) + + describe("if replacement is given", () => { + it("replaces it", () => { + expect(translate("allowed_chars", 5)).toEqual("of 5 chars") + }) + }) + }) + + describe("if translation is not present", () => { + it("Returns passed string", () => { + expect(translate("foo")).toEqual("foo") + }) + }) + }) + + describe("if Alchemy.locale is set to a unknown locale", () => { + beforeEach(() => { + Alchemy.locale = "kl" + }) + + it("Returns passed string and logs a warning", () => { + const spy = jest.spyOn(console, "warn").mockImplementation(() => {}) + expect(translate("help")).toEqual("help") + expect(spy.mock.calls).toEqual([ + ["Translations for locale kl not found!"] + ]) + spy.mockRestore() + }) + }) +}) diff --git a/app/javascript/alchemy/admin/i18n.js b/app/javascript/alchemy/admin/i18n.js new file mode 100644 index 0000000000..a96bca74cb --- /dev/null +++ b/app/javascript/alchemy/admin/i18n.js @@ -0,0 +1,48 @@ +import translationData from "./translations" + +const KEY_SEPARATOR = /\./ + +function currentLocale() { + if (Alchemy.locale == null) { + throw "Alchemy.locale is not set! Please set Alchemy.locale to a locale string in order to translate something." + } + return Alchemy.locale +} + +function getTranslations() { + const locale = currentLocale() + const translations = translationData[locale] + + if (translations) { + return translations + } + console.warn(`Translations for locale ${locale} not found!`) + return {} +} + +function nestedTranslation(translations, key) { + const keys = key.split(KEY_SEPARATOR) + const group = translations[keys[0]] + if (group) { + return group[keys[1]] || key + } + return key +} + +function getTranslation(key) { + const translations = getTranslations() + + if (KEY_SEPARATOR.test(key)) { + return nestedTranslation(translations, key) + } + return translations[key] || key +} + +export default function translate(key, replacement) { + let translation = getTranslation(key) + + if (replacement) { + return translation.replace(/%\{.+\}/, replacement) + } + return translation +} diff --git a/app/javascript/alchemy/admin/translations.js b/app/javascript/alchemy/admin/translations.js new file mode 100644 index 0000000000..602e29fee7 --- /dev/null +++ b/app/javascript/alchemy/admin/translations.js @@ -0,0 +1,32 @@ +const translations = { + en: { + allowed_chars: "of %{count} chars", + cancel: "Cancel", + cancelled: "Cancelled", + click_to_edit: "click to edit", + complete: "Complete", + element_dirty_notice: + "This element has unsaved changes. Do you really want to fold it?", + help: "Help", + ok: "Ok", + page_dirty_notice: + "You have unsaved changes on this page. They will be lost if you continue.", + page_found: "Page found", + pages_found: "Pages found", + url_validation_failed: "The url has no valid format.", + warning: "Warning!", + "File is too large": "File is too large", + "File is too small": "File is too small", + "File type not allowed": "File type not allowed", + "Maximum number of files exceeded": "Maximum number of files exceeded.", + "Uploaded bytes exceed file size": "Uploaded bytes exceed file size", + formats: { + datetime: "Y-m-d H:i", + date: "Y-m-d", + time: "H:i", + time_24hr: false + } + } +} + +export default translations diff --git a/app/javascript/packs/alchemy/admin.js b/app/javascript/packs/alchemy/admin.js new file mode 100644 index 0000000000..142bbc44ed --- /dev/null +++ b/app/javascript/packs/alchemy/admin.js @@ -0,0 +1,12 @@ +import translate from "alchemy/admin/i18n" + +// Global Alchemy object +if (typeof window.Alchemy === "undefined") { + window.Alchemy = {} +} + +// Global utility method for translating a given string +// +Alchemy.t = (key, replacement) => { + return translate(key, replacement) +} diff --git a/app/views/layouts/alchemy/admin.html.erb b/app/views/layouts/alchemy/admin.html.erb index d8ac6b27f1..79bf6394e0 100644 --- a/app/views/layouts/alchemy/admin.html.erb +++ b/app/views/layouts/alchemy/admin.html.erb @@ -36,6 +36,7 @@ <%= render 'alchemy/admin/partials/routes' %> <%= javascript_include_tag('alchemy/admin/all', 'data-turbolinks-track' => true) %> + <%= javascript_pack_tag('alchemy/admin') %> <%= yield :javascript_includes %> <%= content_tag :body, id: 'alchemy', class: alchemy_body_class do %> diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..b59db95bbd --- /dev/null +++ b/babel.config.js @@ -0,0 +1,72 @@ +module.exports = function (api) { + var validEnv = ["development", "test", "production"] + var currentEnv = api.env() + var isDevelopmentEnv = api.env("development") + var isProductionEnv = api.env("production") + var isTestEnv = api.env("test") + + if (!validEnv.includes(currentEnv)) { + throw new Error( + "Please specify a valid `NODE_ENV` or " + + '`BABEL_ENV` environment variables. Valid values are "development", ' + + '"test", and "production". Instead, received: ' + + JSON.stringify(currentEnv) + + "." + ) + } + + return { + presets: [ + isTestEnv && [ + "@babel/preset-env", + { + targets: { + node: "current" + } + } + ], + (isProductionEnv || isDevelopmentEnv) && [ + "@babel/preset-env", + { + forceAllTransforms: true, + useBuiltIns: "entry", + corejs: 3, + modules: false, + exclude: ["transform-typeof-symbol"] + } + ] + ].filter(Boolean), + plugins: [ + "babel-plugin-macros", + "@babel/plugin-syntax-dynamic-import", + isTestEnv && "babel-plugin-dynamic-import-node", + "@babel/plugin-transform-destructuring", + [ + "@babel/plugin-proposal-class-properties", + { + loose: true + } + ], + [ + "@babel/plugin-proposal-object-rest-spread", + { + useBuiltIns: true + } + ], + [ + "@babel/plugin-transform-runtime", + { + helpers: false, + regenerator: true, + corejs: false + } + ], + [ + "@babel/plugin-transform-regenerator", + { + async: false + } + ] + ].filter(Boolean) + } +} diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 0000000000..008ecb22f7 --- /dev/null +++ b/bin/webpack @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 0000000000..a931a9b7fc --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 0000000000..c5edff94ad --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 0000000000..d16d9af743 --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 0000000000..be0f53aacf --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'production' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 0000000000..c5edff94ad --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 0000000000..f5203b1dc0 --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,95 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: alchemy-packs + cache_path: tmp/cache/webpacker + check_yarn_integrity: false + webpack_compile_output: false + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: false + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Verifies that versions and hashed value of the package contents in the project's package.json + check_yarn_integrity: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: alchemy-packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/lib/alchemy/engine.rb b/lib/alchemy/engine.rb index c041313997..60453a41e1 100644 --- a/lib/alchemy/engine.rb +++ b/lib/alchemy/engine.rb @@ -36,6 +36,27 @@ class Engine < Rails::Engine end end + if Rails.env.development? + initializer "alchemy.webpacker.proxy" do |app| + app.middleware.insert_before( + 0, Webpacker::DevServerProxy, + ssl_verify_none: true, + webpacker: Alchemy.webpacker + ) + end + end + + # Serve webpacks if public file server enabled + initializer 'alchemy.webpacker.middleware' do |app| + if app.config.public_file_server.enabled + app.middleware.use( + Rack::Static, + urls: ['/alchemy-packs', '/alchemy-packs-test'], + root: Alchemy::ROOT_PATH.join('public') + ) + end + end + config.after_initialize do require_relative './userstamp' end diff --git a/lib/alchemy/webpacker.rb b/lib/alchemy/webpacker.rb new file mode 100644 index 0000000000..680fa180af --- /dev/null +++ b/lib/alchemy/webpacker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'webpacker' +require 'webpacker/instance' + +module Alchemy + def self.webpacker + @webpacker ||= ::Webpacker::Instance.new( + root_path: ROOT_PATH, + config_path: ROOT_PATH.join("config/webpacker.yml") + ) + end +end diff --git a/lib/alchemy_cms.rb b/lib/alchemy_cms.rb index 73df9914f3..6259ff6d92 100644 --- a/lib/alchemy_cms.rb +++ b/lib/alchemy_cms.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true # Instantiate the global Alchemy namespace module Alchemy - Alchemy::YAML_WHITELIST_CLASSES = %w(Symbol Date Regexp) + YAML_WHITELIST_CLASSES = %w(Symbol Date Regexp) + ROOT_PATH = Pathname.new(File.join(__dir__, "..")) end # Require globally used external libraries @@ -54,6 +55,7 @@ module Alchemy require_relative 'alchemy/resource' require_relative 'alchemy/tinymce' require_relative 'alchemy/taggable' +require_relative 'alchemy/webpacker' # Require hacks require_relative 'kaminari/scoped_pagination_url_helper' diff --git a/lib/tasks/alchemy/install.rake b/lib/tasks/alchemy/install.rake index 27fd3d62ff..06dcf7a8da 100644 --- a/lib/tasks/alchemy/install.rake +++ b/lib/tasks/alchemy/install.rake @@ -30,7 +30,7 @@ end namespace :alchemy do desc "Installs Alchemy CMS into your app." - task :install do + task install: "alchemy:yarn:install" do install_helper = Alchemy::InstallTask.new puts "\nAlchemy Installer" diff --git a/lib/tasks/alchemy/webpacker.rake b/lib/tasks/alchemy/webpacker.rake new file mode 100644 index 0000000000..1bb1ed071b --- /dev/null +++ b/lib/tasks/alchemy/webpacker.rake @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +namespace :alchemy do + namespace :yarn do + desc "Install Alchemy JavaScript dependencies as specified via Yarn" + task :install do + Dir.chdir(File.join(__dir__, "../..")) do + puts "🧙‍♂️ Install AlchemyCMS JS bundle" + system "yarn install --no-progress --production" + end + end + end + + namespace :webpacker do + desc "Compile Alchemy JavaScript packs using webpack for production with digests" + task compile: :environment do + require "fileutils" + Webpacker.with_node_env("production") do + start = Time.now + puts "🧙‍♂️ Compile AlchemyCMS JS packs" + if Alchemy.webpacker.commands.compile + FileUtils.cp_r( + Alchemy::Engine.root.join("public", "alchemy-packs"), + Rails.root.join("public") + ) + else + # Failed compilation + exit! + end + puts "🧙‍♂️ Done in #{(Time.now - start).round(2)}s." + end + end + end +end + +# Compile packs after compiled all other assets during precompilation +if Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance do + Rake::Task["alchemy:webpacker:compile"].invoke + end +else + Rake::Task.define_task("assets:precompile" => "alchemy:webpacker:compile") +end + +if Rake::Task.task_defined?("yarn:install") + Rake::Task["yarn:install"].enhance do + Rake::Task["alchemy:yarn:install"].invoke + end +else + Rake::Task.define_task("yarn:install" => "alchemy:yarn:install") +end diff --git a/package.json b/package.json new file mode 100644 index 0000000000..c189675af2 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@alchemy_cms/admin", + "version": "0.1.0", + "description": "AlchemyCMS", + "browser": "app/javascript/packs/alchemy/admin.js", + "files": [ + "app/javascript/**/*" + ], + "directories": { + "lib": "app/javascript" + }, + "scripts": { + "test": "jest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AlchemyCMS/alchemy_cms.git" + }, + "keywords": [], + "author": "Thomas von Deyen", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/AlchemyCMS/alchemy_cms/issues" + }, + "homepage": "https://github.com/AlchemyCMS/alchemy_cms#readme", + "dependencies": { + "@rails/webpacker": "^5.0.1", + "core-js": "^3" + }, + "devDependencies": { + "babel-jest": "^25.2.6", + "jest": "^25.2.7", + "prettier": "^2.0.2", + "webpack": "^4.42.1", + "webpack-dev-server": "^3.10.3" + }, + "jest": { + "globals": { + "Alchemy": {} + }, + "roots": [ + "app/javascript" + ] + } +} diff --git a/spec/dummy/config/initializers/content_security_policy.rb b/spec/dummy/config/initializers/content_security_policy.rb index 41c43016f1..4a9aad6851 100644 --- a/spec/dummy/config/initializers/content_security_policy.rb +++ b/spec/dummy/config/initializers/content_security_policy.rb @@ -4,17 +4,20 @@ # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https +Rails.application.config.content_security_policy do |policy| + # policy.default_src :self, :https + # policy.font_src :self, :https, :data + # policy.img_src :self, :https, :data + # policy.object_src :none + # policy.script_src :self, :https + # policy.style_src :self, :https -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end + # Specify URI for violation reports + # policy.report_uri "/csp-violation-report-endpoint" + if Rails.env.development? + policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" + end +end # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } diff --git a/spec/dummy/config/webpacker.yml b/spec/dummy/config/webpacker.yml new file mode 100644 index 0000000000..8581ac0472 --- /dev/null +++ b/spec/dummy/config/webpacker.yml @@ -0,0 +1,96 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + check_yarn_integrity: false + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: false + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules + check_yarn_integrity: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + pretty: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true