diff --git a/README.md b/README.md index 1b9f2104..46bf5f91 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ bin/rails g * [Tailored Select](./lib/generators/rolemodel/tailored_select) * [Lograge](./lib/generators/rolemodel/lograge) +### LightningCAD + +For LightningCAD apps there are [generators](./lib/generators/lightning_cad) to add the required setup + ## Development Install the versions of Node and Ruby specified in `.node-version` and `.ruby-version` on your machine. https://asdf-vm.com/ is a great tool for managing language versions. Then run `npm install -g yarn`. diff --git a/lib/generators/lightning_cad/README.md b/lib/generators/lightning_cad/README.md new file mode 100644 index 00000000..90c799ae --- /dev/null +++ b/lib/generators/lightning_cad/README.md @@ -0,0 +1,31 @@ +# LightningCAD Generator + +Run all the generators: + +``` +rails g lightning_cad:all +``` + +Or run them individually +``` +rails g lightning_cad:install +rails g lightning_cad:webpack +rails g lightning_cad:test +``` + +## Depends On + +- `rolemodel:linters:eslint` +- `rolemodel:optics:base` +- `rolemodel:webpack` +- `rolemodel:react` + +## What you get + +Pulls in [LightningCAD](https://github.com/RoleModel/lightning-cad) and sets up a demo editor + +This will add a route for `/editor` with a blank canvas. + +The main entrypoint into the JavaScript code is `/app/javascript/components/App.jsx`. In this file a project is created. You can manually add components like lines or shapes to that project. + +After running the generators additional setup is required see [how to setup a LightningCAD app](https://github.com/RoleModel/lightning-cad?tab=readme-ov-file#app-setup) for more details. diff --git a/lib/generators/lightning_cad/USAGE b/lib/generators/lightning_cad/USAGE new file mode 100644 index 00000000..a9f6e794 --- /dev/null +++ b/lib/generators/lightning_cad/USAGE @@ -0,0 +1,15 @@ +Description: + This generator requires JavaScript setup including webpack, react, and eslint + + Install: + Installs required dependencies + Generates a demo editor + + Webpack: + Modifies the webpack config to work better for LightningCAD apps + + Test: + Installs jasmine and adds the test setup required for LightningCAD models + +Example: + bin/rails generate lightning-cad:all diff --git a/lib/generators/lightning_cad/all_generator.rb b/lib/generators/lightning_cad/all_generator.rb new file mode 100644 index 00000000..932ae15c --- /dev/null +++ b/lib/generators/lightning_cad/all_generator.rb @@ -0,0 +1,13 @@ +require 'rails' + +module LightningCad + class AllGenerator < Rails::Generators::Base + source_root File.expand_path('./templates', __dir__) + + def run_all_the_generators + generate 'lightning_cad:install' + generate 'lightning_cad:webpack' + generate 'lightning_cad:test' + end + end +end diff --git a/lib/generators/lightning_cad/install_generator.rb b/lib/generators/lightning_cad/install_generator.rb new file mode 100644 index 00000000..e6ff216b --- /dev/null +++ b/lib/generators/lightning_cad/install_generator.rb @@ -0,0 +1,81 @@ +require 'rails' + +module LightningCad + class InstallGenerator < Rails::Generators::Base + source_root File.expand_path('./templates', __dir__) + + def install_yarn_dependencies + say 'Adding lightning-cad dependency' + copy_file '.npmrc', '.npmrc' + + dependencies = %w[ + @rolemodel/lightning-cad + @rolemodel/lightning-cad-ui + mobx-react@^6.1.5 + mobx-utils@^5.5.2 + mobx@^5.15.2 + glob@^10.2.2 + import-glob@1.5.0 + react-router-dom@^5.0.1 + react-popper@^1.3.7 + classnames@^2.2.5 + ] + + + run "yarn add #{dependencies.join(" ")}" + end + + def create_basic_app + say "Creating React App Component" + insert_into_file 'app/javascript/controllers/react_controller.js', "import App from '../components/App.jsx'\n", before: "import HelloReact from '../components/HelloReact.jsx'\n" + insert_into_file 'app/javascript/controllers/react_controller.js', " App,\n", after: "const registeredComponents = {\n" + copy_file 'app/javascript/components/MaterialIcon.jsx', 'app/javascript/components/MaterialIcon.jsx' + copy_file 'app/javascript/components/LocalIconFactory.jsx', 'app/javascript/components/LocalIconFactory.jsx' + copy_file 'app/javascript/components/App.jsx', 'app/javascript/components/App.jsx' + end + + def add_stylesheets + stylesheets = <<~CSS + @import '@rolemodel/lightning-cad-ui/scss/lightning-cad.scss'; + CSS + + prepend_to_file 'app/assets/stylesheets/application.scss', stylesheets + end + + def global_configuration + copy_file '.eslintrc.js', '.eslintrc.js' + end + + def create_controller + say "Creating Rails controller and view" + template 'app/views/layouts/editor.html.slim' + copy_file 'app/controllers/editor_controller.rb', 'app/controllers/editor_controller.rb' + copy_file 'app/views/editor/editor.html.slim', 'app/views/editor/editor.html.slim' + route "get '/editor/*all', to: 'editor#editor'" + route "get :editor, to: 'editor#editor'" + end + + def add_javascript_initializers + say "Adding JavaScript initializers" + initializer_setup = <<~JS + import './config/initializers/**/*.js' + JS + append_to_file 'app/javascript/application.js', initializer_setup + copy_file 'app/javascript/config/initializers/smartJSON.js', 'app/javascript/config/initializers/smartJSON.js' + copy_file 'app/javascript/helpers/hooks.js', 'app/javascript/helpers/hooks.js' + copy_file 'app/javascript/helpers/register_hooks.js', 'app/javascript/helpers/register_hooks.js' + # To prevent smartJSON from throwing an error + run 'mkdir app/javascript/shared' + run 'mkdir app/javascript/shared/domain-models' + run 'touch app/javascript/shared/domain-models/.keep' + end + + def setup_chrome_cad + chrome_cad_setup = <<~JS + import { ChromeCADEventEmitter } from '@rolemodel/lightning-cad/drawing-editor' + window.__LCAD_CHROME_CAD_EVENT_EMITTER = new ChromeCADEventEmitter() + JS + append_to_file 'app/javascript/application.js', chrome_cad_setup + end + end +end diff --git a/lib/generators/lightning_cad/templates/.eslintrc.js b/lib/generators/lightning_cad/templates/.eslintrc.js new file mode 100644 index 00000000..f0f5e0a8 --- /dev/null +++ b/lib/generators/lightning_cad/templates/.eslintrc.js @@ -0,0 +1,123 @@ +export default { + root: true, + env: { + browser: true, + node: true, + es6: true, + }, + plugins: ["react"], + extends: ["eslint:recommended", "plugin:react/recommended"], + parser: "babel-eslint", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2017, + }, + rules: { + "accessor-pairs": "warn", + "arrow-body-style": "warn", + "arrow-parens": ["warn", "as-needed"], + "arrow-spacing": "warn", + "comma-dangle": ["warn", "only-multiline"], + "comma-style": ["warn", "last"], + "computed-property-spacing": ["warn", "never"], + "consistent-this": "warn", + curly: ["warn", "multi-line"], + "default-case": "warn", + "dot-location": ["warn", "property"], + eqeqeq: "warn", + "generator-star-spacing": "warn", + "id-blacklist": "warn", + "id-match": "warn", + "jsx-quotes": "warn", + "linebreak-style": ["warn", "unix"], + "max-nested-callbacks": "warn", + "no-array-constructor": "warn", + "no-caller": "warn", + "no-catch-shadow": "warn", + "no-console": "off", + "no-continue": "warn", + "no-div-regex": "warn", + "no-duplicate-imports": "warn", + "no-extra-label": "warn", + "no-extra-semi": "warn", + "no-floating-decimal": "warn", + "no-implicit-coercion": [ + "warn", + { + boolean: false, + number: false, + string: false, + }, + ], + "no-implied-eval": "warn", + "no-inner-declarations": ["warn", "functions"], + "no-invalid-this": "warn", + "no-iterator": "warn", + "no-label-var": "warn", + "no-labels": "warn", + "no-lone-blocks": "warn", + "no-mixed-requires": "warn", + "no-mixed-spaces-and-tabs": "warn", + "no-multi-str": "warn", + "no-new": "warn", + "no-new-func": "warn", + "no-new-object": "warn", + "no-new-require": "warn", + "no-new-wrappers": "warn", + "no-octal-escape": "warn", + "no-process-exit": "warn", + "no-proto": "warn", + "no-redeclare": "warn", + "no-restricted-globals": "warn", + "no-restricted-imports": "warn", + "no-restricted-modules": "warn", + "no-restricted-syntax": "warn", + "no-script-url": "warn", + "no-self-compare": "warn", + "no-shadow-restricted-names": "warn", + "no-spaced-func": "warn", + "no-undef": "warn", + "no-undef-init": "warn", + "no-unexpected-multiline": "warn", + "no-unmodified-loop-condition": "warn", + "no-unneeded-ternary": [ + "warn", + { + defaultAssignment: true, + }, + ], + "no-unreachable": "warn", + "no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", // Allow unused arguments starting with an underscore + varsIgnorePattern: "^_", // Allow unused variables starting with an underscore + }, + ], + "no-useless-call": "warn", + "no-useless-concat": "warn", + "no-useless-constructor": "warn", + "no-useless-escape": "warn", + "no-var": "warn", + "no-void": "warn", + "no-whitespace-before-property": "warn", + "no-with": "warn", + "prefer-arrow-callback": "warn", + "prefer-const": "warn", + "prefer-template": "warn", + "require-yield": "warn", + semi: ["warn", "never"], + "sort-imports": "warn", + "template-curly-spacing": "warn", + "wrap-regex": "warn", + "yield-star-spacing": "warn", + yoda: ["warn", "never"], + }, + settings: { + react: { + version: "16.6.0", + }, + }, +}; diff --git a/lib/generators/lightning_cad/templates/.npmrc b/lib/generators/lightning_cad/templates/.npmrc new file mode 100644 index 00000000..6376c0b3 --- /dev/null +++ b/lib/generators/lightning_cad/templates/.npmrc @@ -0,0 +1,2 @@ +@rolemodel:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN} diff --git a/lib/generators/lightning_cad/templates/app/controllers/editor_controller.rb b/lib/generators/lightning_cad/templates/app/controllers/editor_controller.rb new file mode 100644 index 00000000..b9d616bd --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/controllers/editor_controller.rb @@ -0,0 +1,5 @@ +class EditorController < ApplicationController + def editor + render layout: 'editor' + end +end diff --git a/lib/generators/lightning_cad/templates/app/javascript/components/App.jsx b/lib/generators/lightning_cad/templates/app/javascript/components/App.jsx new file mode 100644 index 00000000..0b246c1d --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/components/App.jsx @@ -0,0 +1,86 @@ +import React from 'react' +import { createBrowserHistory } from 'history' +import PropTypes from 'prop-types' + +import { + Icon, + IconFactoryContext, + MultiPerspectiveProjectEditorView, +} from '@rolemodel/lightning-cad-ui' +import { Router } from 'react-router-dom' +import LocalIconFactory from './LocalIconFactory.jsx' + +import { + DrawingEditor, + VersionedProject, + Project +} from '@rolemodel/lightning-cad/drawing-editor' + +export default class App extends React.Component { + static propTypes = { + basePath: PropTypes.string, + backPath: PropTypes.string + } + + static defaultProps = { + basePath: '/', + backPath: '/' + } + + constructor(props) { + super(props) + this._modalRoot = document.getElementById('modal_root') || document.createElement('div') + + this._project = new VersionedProject(new Project()) + + const top = new DrawingEditor(this._project) + top.toolPalette() + this._drawingEditors = { top } + } + + modalRoot() { return this._modalRoot } + + history() { + if (!this._history) { + this._history = createBrowserHistory({ basename: this.props.basePath }) + } + return this._history + } + + iconFactory() { + return this._iconFactory ??= new LocalIconFactory() + } + + render() { + return ( + + + ( + + + Back + + ), + }, + }, + }} + /> + + + ) + } +} diff --git a/lib/generators/lightning_cad/templates/app/javascript/components/LocalIconFactory.jsx b/lib/generators/lightning_cad/templates/app/javascript/components/LocalIconFactory.jsx new file mode 100644 index 00000000..b45c6828 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/components/LocalIconFactory.jsx @@ -0,0 +1,56 @@ +import React from 'react' +import classnames from 'classnames' + +import { IconFactory } from '@rolemodel/lightning-cad-ui' + +import MaterialIcon from './MaterialIcon.jsx' + +const customIcons = {} + +export default class LocalIconFactory extends IconFactory { + constructor(props) { + super(props); + + this._iconNameAlias = { + KeyboardArrowLeft: "keyboard_arrow_left", + KeyboardArrowRight: "keyboard_arrow_right", + KeyboardArrowDown: "keyboard_arrow_down", + ArrowBack: "keyboard_backspace", + Lock: "lock", + LockOpen: "lock_open", + Visibility: "visibility", + VisibilityOff: "visibility_off", + undo: "arrow_back", + redo: "arrow_forward", + }; + } + + makeIcon(name, otherProps) { + const iconName = this._iconNameAlias[name] || name; + + const iconProps = { + className: otherProps.className, + title: otherProps.hoverText || otherProps.name, + }; + + if (iconName in customIcons) { + return this._customIcon(iconName, iconProps); + } + + return ; + } + + /* eslint-disable react/no-danger */ + _customIcon(iconName, { className, ...otherProps }) { + // Setting innerHTML is not dangerous with these SVG files since we created + // them and they are part of this app's code. + return ( + + ); + } + /* eslint-enable react/no-danger */ +} diff --git a/lib/generators/lightning_cad/templates/app/javascript/components/MaterialIcon.jsx b/lib/generators/lightning_cad/templates/app/javascript/components/MaterialIcon.jsx new file mode 100644 index 00000000..7b934742 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/components/MaterialIcon.jsx @@ -0,0 +1,26 @@ +import React from 'react' +import classnames from 'classnames' +import * as PropTypes from 'prop-types' + +function MaterialIcon({ iconName, iconProps: { className, ...otherProps } }) { + return ( + + {iconName} + + ) +} + +MaterialIcon.propTypes = { + iconName: PropTypes.string, + iconProps: PropTypes.shape({ + className: PropTypes.string, + title: PropTypes.string, + hoverText: PropTypes.string, + name: PropTypes.string + }) +} + +export default MaterialIcon diff --git a/lib/generators/lightning_cad/templates/app/javascript/config/initializers/smartJSON.js b/lib/generators/lightning_cad/templates/app/javascript/config/initializers/smartJSON.js new file mode 100644 index 00000000..ea6106de --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/config/initializers/smartJSON.js @@ -0,0 +1,38 @@ +import { SmartObjectBuilder } from '@rolemodel/lightning-cad/smartJSON' + +let domainModelModules = [] +if (import.meta.webpackContext) { + const domainModelsContext = await import.meta.webpackContext( + '../../shared/domain-models', + { + recursive: true, + regExp: /.js$/, + } + ) + // Usually browser + domainModelModules = domainModelsContext.keys().map(domainModelsContext) +} else { + // Node/test environment + const path = await import('path') + const glob = await import('glob') + const url = await import('url') + + const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + const files = glob.sync('**/*.js', { + cwd: path.resolve(path.join(__dirname, '../../shared/domain-models')) + }) + + domainModelModules = [] + + await Promise.all( + files.map(async (file) => { + domainModelModules.push(await import(`../../shared/domain-models/${file}`)) + }) + ) +} + +const domainModels = domainModelModules.map(({ default: module }) => module) + +SmartObjectBuilder.configure((config) => { + config.classes.addClasses(...domainModels) +}) diff --git a/lib/generators/lightning_cad/templates/app/javascript/helpers/hooks.js b/lib/generators/lightning_cad/templates/app/javascript/helpers/hooks.js new file mode 100644 index 00000000..78bd2307 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/helpers/hooks.js @@ -0,0 +1,84 @@ +import path from 'node:path' +import * as glob from 'glob' +import filterFile from '../../../config/webpack/loaders/filter-file.js' + +const shorthandRoot = path.join(process.cwd(), 'app/javascript') +const config = { + shorthandRoot +} + +export function resolve(specifier, context, nextResolve) { + let finalSpecifier = specifier + if (specifier.startsWith('#')) { + finalSpecifier = resolveShorthandPath(finalSpecifier, context, nextResolve) + } + + if (specifier.includes('*')) { + return resolveGlobPath(finalSpecifier, context, nextResolve) + } + + return nextResolve(finalSpecifier, context) +} + +export async function load(specifier, context, nextLoad) { + if (!specifier.includes('*')) { + const result = await nextLoad(specifier, context) + if (!result.source) return result + + const source = filterFile(result.source, 'server') + + return { + ...result, + source + } + } + + const pattern = specifier.replace('file://', '') + const paths = glob.globSync(pattern, { nodir: true }) + + const pathParts = pattern.split('/') + const basePathParts = pathParts.slice(0, pathParts.findIndex(part => part.includes('*'))) + // we need to determine how many times we have a "*" in a folder level. The imports have to go back that far in order to work properly. + const folderLevels = pathParts.length - basePathParts.length - 1 // minus one for the file + const basePath = basePathParts.join('/') + const baseImportPath = `${Array(folderLevels).fill('../').join('')}` + + const getModuleIdentifier = index => `module${index}` + const getImportPath = file => path.join(baseImportPath, path.relative(basePath, file)) + const importStatements = paths.map((file, index) => { + return `import * as ${getModuleIdentifier(index)} from './${getImportPath(file)}'` + }) + const exportStatement = `export default [${paths.map((_s, index) => getModuleIdentifier(index)).join(', ') }]` + + const content = [...importStatements, exportStatement].join('\n') + const source = Buffer.from(content) + + return { + format: "module", + source, + shortCircuit: true + } +} + +function resolveShorthandPath(specifier, _context, _nextResolve) { + return path.join(config.shorthandRoot, specifier.slice(1)) +} + +function resolveGlobPath(specifier, context, _nextResolve) { + // We have to manually resolve the path so that we can load all the files that match the pattern later + let finalPath + if (specifier.startsWith('.')) { // Relative path to parent + const basePath = context.parentURL.replace('file://', '').split('/').slice(0, -1).join('/') + finalPath = path.join(basePath, specifier) + } else if (specifier.startsWith('/')) { // Absolute path + finalPath = specifier + } else { // node modules + finalPath = path.join(process.cwd(), 'node_modules', specifier) + } + + return { + url: `file://${finalPath}`, + type: "module", + shortCircuit: true + } +} diff --git a/lib/generators/lightning_cad/templates/app/javascript/helpers/register_hooks.js b/lib/generators/lightning_cad/templates/app/javascript/helpers/register_hooks.js new file mode 100644 index 00000000..bf581f21 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/javascript/helpers/register_hooks.js @@ -0,0 +1,3 @@ +import { register } from 'node:module' + +register('./hooks.js', import.meta.url) diff --git a/lib/generators/lightning_cad/templates/app/views/editor/editor.html.slim b/lib/generators/lightning_cad/templates/app/views/editor/editor.html.slim new file mode 100644 index 00000000..3297a591 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/views/editor/editor.html.slim @@ -0,0 +1,2 @@ += react_component 'App', basePath: editor_path +#modal_root diff --git a/lib/generators/lightning_cad/templates/app/views/layouts/editor.html.slim.tt b/lib/generators/lightning_cad/templates/app/views/layouts/editor.html.slim.tt new file mode 100644 index 00000000..a6afe5d7 --- /dev/null +++ b/lib/generators/lightning_cad/templates/app/views/layouts/editor.html.slim.tt @@ -0,0 +1,11 @@ +doctype html +html + head + title <%= Rails.application.class.name.deconstantize.titleize %> + = csrf_meta_tags + = csp_meta_tag + = stylesheet_link_tag 'application', 'data-turbo-track': 'reload' + = javascript_include_tag 'application', 'data-turbo-track': 'reload', defer: true + meta content="width=device-width, initial-scale=1" name="viewport" + body + = yield diff --git a/lib/generators/lightning_cad/templates/config/webpack/loaders/filter-file.js b/lib/generators/lightning_cad/templates/config/webpack/loaders/filter-file.js new file mode 100644 index 00000000..e3effe98 --- /dev/null +++ b/lib/generators/lightning_cad/templates/config/webpack/loaders/filter-file.js @@ -0,0 +1,26 @@ +/** + * Webpack loader to filter server-only or client-only sections of code out of + * a file, based on the current target (e.g., 'web' or 'node') + */ +export default function (source, target = this.target) { + let begin, end + if (target === 'web') { + begin = '// @begin-server-only' + end = '// @end-server-only' + } else { + begin = '// @begin-client-only' + end = '// @end-client-only' + } + + let filteredSource = source + let beginIndex = filteredSource.indexOf(begin) + let endIndex = filteredSource.indexOf(end) + while (beginIndex !== -1) { + const afterExcludedBlock = endIndex === -1 ? '' : filteredSource.slice(endIndex + end.length) + filteredSource = filteredSource.slice(0, beginIndex) + afterExcludedBlock + beginIndex = filteredSource.indexOf(begin) + endIndex = filteredSource.indexOf(end) + } + + return filteredSource +} diff --git a/lib/generators/lightning_cad/templates/jasmine.json b/lib/generators/lightning_cad/templates/jasmine.json new file mode 100644 index 00000000..77dab913 --- /dev/null +++ b/lib/generators/lightning_cad/templates/jasmine.json @@ -0,0 +1,6 @@ +{ + "spec_dir": "spec/javascript", + "spec_files": ["shared/**/*[sS]pec.js", "server/**/*[sS]pec.js"], + "helpers": ["./shared/TestSetup.js", "./server/TestSetup.js"], + "stopSpecOnExpectationFailure": true +} diff --git a/lib/generators/lightning_cad/templates/jp-runner.config.js b/lib/generators/lightning_cad/templates/jp-runner.config.js new file mode 100644 index 00000000..e77b41c1 --- /dev/null +++ b/lib/generators/lightning_cad/templates/jp-runner.config.js @@ -0,0 +1,14 @@ +import DetailedTestFormatter from '@rolemodel/jasmine-playwright-runner/src/server/DetailedTestFormatter.js' + +export default { + specPatterns: [ + 'spec/javascript/browser/**/*Spec.js', + 'spec/javascript/browser/**/*Spec.jsx', + 'spec/javascript/shared/**/*Spec.js' + ], + setupFiles: [ + 'spec/javascript/shared/TestSetup.js', + 'spec/javascript/browser/TestSetup.js' + ], + formatter: new DetailedTestFormatter() +} diff --git a/lib/generators/lightning_cad/templates/lib/tasks/javascript_tests.rake b/lib/generators/lightning_cad/templates/lib/tasks/javascript_tests.rake new file mode 100644 index 00000000..ffc529fb --- /dev/null +++ b/lib/generators/lightning_cad/templates/lib/tasks/javascript_tests.rake @@ -0,0 +1,6 @@ +desc 'run javascript tests' +task javascript_tests: :environment do |t| + success = true + success &&= system('yarn test_shared') + abort('JS tests failed') unless success +end diff --git a/lib/generators/lightning_cad/templates/spec/javascript/.eslintrc.js b/lib/generators/lightning_cad/templates/spec/javascript/.eslintrc.js new file mode 100644 index 00000000..560fa983 --- /dev/null +++ b/lib/generators/lightning_cad/templates/spec/javascript/.eslintrc.js @@ -0,0 +1,5 @@ +export default { + env: { + jasmine: true + } +} diff --git a/lib/generators/lightning_cad/templates/spec/javascript/browser/TestSetup.js b/lib/generators/lightning_cad/templates/spec/javascript/browser/TestSetup.js new file mode 100644 index 00000000..89bcfaa6 --- /dev/null +++ b/lib/generators/lightning_cad/templates/spec/javascript/browser/TestSetup.js @@ -0,0 +1 @@ +// for browser only test setup diff --git a/lib/generators/lightning_cad/templates/spec/javascript/server/TestSetup.js b/lib/generators/lightning_cad/templates/spec/javascript/server/TestSetup.js new file mode 100644 index 00000000..d1e95c66 --- /dev/null +++ b/lib/generators/lightning_cad/templates/spec/javascript/server/TestSetup.js @@ -0,0 +1 @@ +// for server only test setup diff --git a/lib/generators/lightning_cad/templates/spec/javascript/shared/TestSetup.js b/lib/generators/lightning_cad/templates/spec/javascript/shared/TestSetup.js new file mode 100644 index 00000000..9e1a00a6 --- /dev/null +++ b/lib/generators/lightning_cad/templates/spec/javascript/shared/TestSetup.js @@ -0,0 +1,9 @@ +import "@rolemodel/lightning-cad/standard-utilities" + +import '../../../app/javascript/config/initializers/**/*.js' + +import EqualityHelper from '@rolemodel/lightning-cad/standard-utilities/spec/helpers/EqualityHelper' +import ToIncludeHelper from '@rolemodel/lightning-cad/standard-utilities/spec/helpers/ToIncludeHelper' + +beforeAll(EqualityHelper) +beforeAll(ToIncludeHelper) diff --git a/lib/generators/lightning_cad/templates/spec/javascript/shared/testSpec.js b/lib/generators/lightning_cad/templates/spec/javascript/shared/testSpec.js new file mode 100644 index 00000000..4ea8e277 --- /dev/null +++ b/lib/generators/lightning_cad/templates/spec/javascript/shared/testSpec.js @@ -0,0 +1,5 @@ +describe('Foo', () => { + it('works', () => { + expect().nothing() + }) +}) diff --git a/lib/generators/lightning_cad/test_generator.rb b/lib/generators/lightning_cad/test_generator.rb new file mode 100644 index 00000000..10b0e89c --- /dev/null +++ b/lib/generators/lightning_cad/test_generator.rb @@ -0,0 +1,24 @@ +require 'rails' + +module LightningCad + class TestGenerator < Rails::Generators::Base + source_root File.expand_path('./templates', __dir__) + + def add_jasmine_tests + say 'Adding jasmine' + run "yarn add --dev jasmine@^5.1.0 @rolemodel/jasmine-playwright-runner @testing-library/react" + + run 'npm pkg set scripts.test:server="NODE_OPTIONS=\'--import=./app/javascript/helpers/register_hooks.js\' jasmine --config=jasmine.json"' + run 'npm pkg set scripts.test:browser="NODE_ENV=test jp-runner"' + run 'npm pkg set scripts.test="yarn test:server && yarn test:browser"' + + copy_file 'jasmine.json' + copy_file 'jp-runner.config.js' + copy_file 'spec/javascript/.eslintrc.js' + copy_file 'spec/javascript/shared/TestSetup.js' + copy_file 'spec/javascript/browser/TestSetup.js' + copy_file 'spec/javascript/server/TestSetup.js' + copy_file 'spec/javascript/shared/testSpec.js' + end + end +end diff --git a/lib/generators/lightning_cad/webpack_generator.rb b/lib/generators/lightning_cad/webpack_generator.rb new file mode 100644 index 00000000..bf329d90 --- /dev/null +++ b/lib/generators/lightning_cad/webpack_generator.rb @@ -0,0 +1,99 @@ +require 'rails' + +module LightningCad + class WebpackGenerator < Rails::Generators::Base + source_root File.expand_path('./templates', __dir__) + + def add_experimental_features + say "Adding experimental features to the config" + + experiments = <<-JS + experiments: { + topLevelAwait: true + }, + JS + + insert_into_file 'webpack.config.js', experiments, before: " output: {" + end + + def add_resolve_fallback + fallback = ",\n fallback: {\n module: false\n }" + + insert_into_file 'webpack.config.js', fallback, after: "extensions: ['.js', '.jsx']" + end + + def add_resolve_aliases + say 'Adding aliases to the config' + alias_js = <<-JS + alias: { + // lightning-cad uses 'require' to pull in THREE, but three-bvh-csg uses 'import'. + // Because THREE's package.json has an exports field with different files for + // import and require, without this alias THREE was being added to the bundle twice. + // Not only is that a size problem, but the custom extensions lightning-cad adds to + // CJS version of THREE aren't picked up because the ESM version of THREE appears first + // in the bundle and THREE has a check to prevent THREE from being instantiated twice. + // + 'three/examples': path.resolve('node_modules/three/examples'), + // TODO: Once lightning-cad migrates to using 'import' to bring in THREE, this alias can be removed. + three: path.resolve('node_modules/three/build/three.module.js'), + + // LCAD uses `require` to bring in mathjs, but rails 7 apps uses `import`, so there are + // two mathjs module instances in play, and without this line both end up in the final bundle. + // TODO: Once LCAD migrates to ES Modules, this line can be removed. + mathjs: path.resolve('node_modules/mathjs/lib/esm'), + + // Webpack apparently doesn't support the * in package.imports, so we need to duplicate + // package.imports here for webpack. + '#components': path.resolve('app/javascript/components'), + '#shared': path.resolve('app/javascript/shared') + }, + JS + + insert_into_file 'webpack.config.js', alias_js, before: " fallback: {" + end + + def add_loaders + say 'Adding loaders to the config' + + copy_file 'config/webpack/loaders/filter-file.js' + + loaders = <<-JS +, + { + test: /\.jsx?$/, + use: path.resolve('./config/webpack/loaders/filter-file.js') + }, + { + test: /\.(mjs|cjs|js|jsx)$/, + loader: 'import-glob' + }, + { + test: /\.scss/, + loader: 'import-glob' + }, + JS + + insert_into_file 'webpack.config.js', loaders, after: "'postcss-loader'\n ]\n }" + end + + def add_terser_plugin_options + say 'Updating the terser plugin options in the config' + + terserPlugin = <<-JS + new TerserPlugin({ + terserOptions: { + keep_classnames: true, + mangle: { + keep_fnames: /^[A-Z]/, + }, + compress: { + keep_fnames: false, + } + } + }), + JS + + gsub_file 'webpack.config.js', " new TerserPlugin(),\n", terserPlugin + end + end +end diff --git a/lib/generators/rolemodel/webpack/templates/webpack.config.js b/lib/generators/rolemodel/webpack/templates/webpack.config.js index 2b91953d..86fc2688 100644 --- a/lib/generators/rolemodel/webpack/templates/webpack.config.js +++ b/lib/generators/rolemodel/webpack/templates/webpack.config.js @@ -44,7 +44,7 @@ export default { loader: 'esbuild-loader', options: { loader: 'jsx', - target: 'es2021' + target: 'esnext' }, // ES Module has stricter rules than CommonJS, so file extensions must be // used in import statements. To ease migration from CommonJS to ESM,