diff --git a/.eslintrc.json b/.eslintrc.json index c021991..6c767e8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,38 +2,30 @@ "env": { "es6": true, "node": true, - "browser": true, - "jest": true + "browser": true }, "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2022, "sourceType": "module" }, "globals": { "L": true, "W": true, - "console": true, - "cordova": true, - "riot": true, - "opts": true, - "d3": true, - "omnivore": true + "console": true }, "extends": "eslint:recommended", "rules": { - "indent":"off", + "indent": "off", "curly": ["error", "all"], "dot-location": ["error", "property"], - "no-cond-assign":"off", - "no-console":"off", - "no-fallthrough":"off", - "no-mixed-spaces-and-tabs":"off", + "no-cond-assign": "off", + "no-console": "off", + "no-fallthrough": "off", + "no-mixed-spaces-and-tabs": "off", "no-multi-spaces": "error", - "no-undef-init":"error", - "no-undef":"error", + "no-undef-init": "error", + "no-undef": "error", "semi": "error" }, - "plugins": [ - "html" - ] + "plugins": ["html"] } diff --git a/compiler.js b/compiler.js index 14742d0..dbfc19d 100755 --- a/compiler.js +++ b/compiler.js @@ -4,26 +4,33 @@ * This is plugin building script. Feel free to modify it * All is MIT licenced */ -const prog = require('commander'); -const { join } = require('path'); -const c = require('consola'); -const fs = require('fs-extra'); -const { yellow, gray } = require('colorette'); -const riot = require('riot-compiler'); -const assert = require('assert'); -const express = require('express'); -const app = express(); -const less = require('less'); -const chokidar = require('chokidar'); -const decache = require('decache'); -const https = require('https'); -const babel = require('@babel/core'); - -const utils = require('./dev/utils.js'); +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import assert from 'node:assert'; +import https from 'node:https'; + +import prog from 'commander'; +import prompts from 'prompts'; +import ucfirst from 'ucfirst'; +import c from 'consola'; +import { yellow, gray } from 'colorette'; +import express from 'express'; +import chokidar from 'chokidar'; +import decache from 'decache'; + +import { builder } from './dev/rollup.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const app = express(); const port = 9999; -const { version, name, author, repository, description } = require('./package.json'); +// TODO add to plugin +const { version, name, author, repository, description } = JSON.parse( + fs.readFileSync(path.join(__dirname, 'package.json')), +); prog.option('-b, --build', 'Build the plugin in required directory (default src)') .option('-w, --watch', 'Build plugin and watch file changes in required directory') @@ -40,16 +47,62 @@ if (!process.argv.slice(2).length) { let config, srcDir = 'src'; +export const prompt = async () => { + let dir = 'src'; + + const list = fs.readdirSync(path.join(__dirname, 'examples')).filter(d => /\d\d-/.test(d)); + + console.log(`\nSelect which example you want to test:\n`); + + list.map((d, i) => + console.log(` ${yellow(i + 1)}) ${ucfirst(d.replace(/^\d\d-/, '').replace(/-/g, ' '))}`), + ); + + console.log( + `\n ${yellow(0)}) F***K OFF with examples. I am pro. I want to develop ${yellow( + 'my own plugin', + )}.\n`, + ); + + let { value } = await prompts({ + type: 'number', + name: 'value', + message: `Which example you want to launch? (press 0 - ${list.length}):`, + validate: value => (value >= 0 && value < list.length + 1 ? true : false), + }); + + if (value > 0) { + dir = path.join('examples', list[value - 1]); + } else if (value === 0) { + console.log(`---------------------------------------------------- + Please change ${yellow('package.json')} now: + + ${yellow('name')}: Must contain name of your plugin in a form windy-plugin-AnyName + ${yellow('description')}: Should be description of what your plugin does + ${yellow('author')}: Should contain your name + ${yellow('repository')}: Should be actual link to your hosting repo + + Also ${yellow('./README.md')} should contain some info about your plugin if you wish + + For faster work use directlly ${yellow('npm run start-dev')} to skip this prompt + + After you will be done use ${yellow('npm publish')} to publish your plugin. + -----------------------------------------------------`); + } + + return dir; +}; + // Main (async () => { console.log(`\nBuilding ${yellow(name)}, version ${yellow(version)}`); // Beginners example selection if (prog.prompt) { - srcDir = await utils.prompt(); + srcDir = await prompt(); } - c.info(`Compiler will compile ${yellow(`./${srcDir}/plugin.html`)}`); + c.info(`Compiler will compile ${yellow(`./${srcDir}/`)}`); await reloadConfig(); @@ -97,8 +150,8 @@ function startServer() { return new Promise(resolve => { const httpsOptions = { // https://www.ibm.com/support/knowledgecenter/en/SSWHYP_4.0.0/com.ibm.apimgmt.cmc.doc/task_apionprem_gernerate_self_signed_openSSL.html - key: fs.readFileSync(join(__dirname, 'dev', 'key.pem'), 'utf8'), - cert: fs.readFileSync(join(__dirname, 'dev', 'certificate.pem'), 'utf8'), + key: fs.readFileSync(path.join(__dirname, 'dev', 'key.pem'), 'utf8'), + cert: fs.readFileSync(path.join(__dirname, 'dev', 'certificate.pem'), 'utf8'), }; app.use(express.static('dist')); @@ -123,121 +176,30 @@ function startServer() { */ async function build() { - // Riot parser options - const riotOpts = { - entities: true, - compact: false, - expr: true, - type: null, - template: null, - fileConfig: null, - concat: false, - modular: false, - debug: true, + const destination = path.join(__dirname, 'dist'); + const meta = { + name, + version, + author, + repository, + description, + ...config, }; - // Compile less - feel free to code your SCSS here - let css = await compileLess(); - - // Load source code of a plugin - const tagSrc = await fs.readFile(join(srcDir, 'plugin.html'), 'utf8'); - - // Compile it via riot compiler - // See: https://github.com/riot/compiler - const [compiled] = riot.compile(tagSrc, riotOpts); - let { html, js, imports } = compiled; - - const options = Object.assign( - {}, - { - name, - version, - author, - repository, - description, - }, - config, - ); - - const internalModules = {}; - - // - // Rewrite imports into W.require - // - if (imports) { - let match; - const importsRegEx = /import\s+(?:\*\s+as\s+)?(\{[^}]+\}|\S+)\s+from\s+['"](@windy\/)?(plugins\/)?([^'"']+)['"]/gm; - while ((match = importsRegEx.exec(imports)) !== null) { - let [, lex, isCore, isPlugin, module] = match; - // detect syntax "import graph from './soundingGraph.mjs'" - // and loads external module - if (!isCore) { - module = await utils.externalMjs(srcDir, internalModules, module, name); - } - js = `\tconst ${lex} = W.require('${(isPlugin ? '@plugins/' : '') + module}');\n${js}`; - } - } - - // Stringify output - let output = utils.stringifyPlugin(options, html, css, js); - - // Add external modules - for (let ext in internalModules) { - output += `\n\n${internalModules[ext]}`; - } - - // Save plugin to dest directory - const destination = join(__dirname, 'dist', 'plugin.js'); - - // Babel traspile - if (prog.transpile) { - c.info('Transpiling with babel'); - let res = await babel.transformAsync(output, { - presets: ['@babel/preset-env'], - }); // => Promise<{ code, map, ast }> - output = res.code; - } + const { code } = await builder(name, srcDir, meta); - await fs.outputFile(destination, output); + fs.writeFileSync(path.join(destination, 'plugin.js'), code); c.success(`Your plugin ${gray(name)} has been compiled to ${gray(destination)}`); } -// -// L E S S compiler -// -async function compileLess() { - const lessOptions = { - cleancss: true, - compress: true, - }; - - const lessFile = join(srcDir, 'plugin.less'); - - if (!fs.existsSync(lessFile)) { - return null; - } - - const lessSrc = await fs.readFile(lessFile, 'utf8'); - - let { css } = await less.render(lessSrc, lessOptions); - - return css; -} - -// -// Reload config -// async function reloadConfig() { - const dir = join(__dirname, srcDir, 'config.js'); + const { default: dir } = await import(path.join(__dirname, srcDir, 'config.js')); decache(dir); - config = require(dir); + config = dir; return; } -// -// Watch change of file -// const onChange = async fullPath => { c.info(`watch: File changed ${gray(fullPath)}`); diff --git a/dev/mjs2js.js b/dev/mjs2js.js deleted file mode 100644 index 96e2ad1..0000000 --- a/dev/mjs2js.js +++ /dev/null @@ -1,151 +0,0 @@ -// .mjs -> es6 -> native js -const fs = require('fs-extra'); -const consola = require('console'); -const { find } = require('./shimport'); - -// Replaces all imports, exports in a file -module.exports = async (fullPath, moduleId, namespace) => { - const body = await fs.readFile(fullPath, 'utf8'); - const transformed = transform(fullPath, body, moduleId, namespace); - - return transformed; -}; - -const transform = (file, source, id, namespace) => { - const [importDeclarations, importStatements, importMetaUrls, exportDeclarations] = find( - source, - id, - ); - - const nameBySource = new Map(); - const externalModules = []; - - importDeclarations.forEach(d => { - if (nameBySource.has(d.source)) return; - if (/@windy\//.test(d.source)) { - // Windy's core module - d.source = d.source.replace(/@windy\/(\S+)/, '$1'); - - if (/plugins\//.test(d.source)) { - d.source = '@' + d.source; - } - } else if (/\.\/\S+\.mjs/.test(d.source)) { - // Plugin's module - - externalModules.push(d.source); - d.source = `${namespace}/${d.source.replace(/\.\/(\S+)\.mjs/, '$1')}`; - } else if (!/@plugins\//.test(d.source)) { - // "@plugins/xyz" is allowed - throw new Error( - 'Unable to import module. Windy plugin compiler is primitive and' + - ' supports only "@windy/name", or "./filename.mjs" modules', - ); - } - nameBySource.set(d.source, d.name || `__dep_${nameBySource.size}`); - }); - - let moduleHasNamedExport = false; - let moduleHaDefaultExport = false; - - exportDeclarations.forEach(d => { - if (!d.name) { - moduleHaDefaultExport = true; - } else { - moduleHasNamedExport = true; - } - - if (!d.source) return; - if (nameBySource.has(d.source)) return; - nameBySource.set(d.source, d.name || `__dep_${nameBySource.size}`); - }); - - if (moduleHaDefaultExport && moduleHasNamedExport) { - consola.error( - `es2Wdefine detected combination of named and default exports in one module: ${id}`, - ); - } - - const deps = Array.from(nameBySource.keys()) - .map(s => `'${s.replace(/^@windy\//, '')}'`) - .join(', '); - - const names = ['__exports'].concat(Array.from(nameBySource.values())).join(', '); - - const hoisted = []; - - importDeclarations.forEach(decl => { - const name = nameBySource.get(decl.source); - let moduleHasNamedImport = false; - let moduleHaDefaultImport = false; - - decl.specifiers - .sort((a, b) => { - if (a.name === 'default') { - return 1; - } - if (b.name === 'default') { - return -1; - } - }) - .forEach(s => { - if (s.name === 'default') { - moduleHaDefaultImport = true; - } else { - moduleHasNamedImport = true; - } - - if (s.name !== '*') { - /** - * Original version combining default & named exports - - const assignment = - s.name === 'default' && s.as === name - ? `${s.as} = ${name}.default; ` - : `var ${s.as} = ${name}.${s.name}; `; - - hoisted.push(assignment); - - */ - - if (s.name !== 'default') { - hoisted.push(`var ${s.as} = ${name}.${s.name}; `); - } - } - }); - if (moduleHaDefaultImport && moduleHasNamedImport) { - consola.error( - `es2Wdefine detected combination of named and default import in one module: ${id}`, - ); - } - }); - - let transformed = `W.define('${id}',\n[${deps}], function(${names}){ \n\n${hoisted.join('\n') + - '\n\n'}`; - - const ranges = [ - ...importDeclarations, - ...importStatements, - ...importMetaUrls, - ...exportDeclarations, - ].sort((a, b) => a.start - b.start); - - let c = 0; - - for (let i = 0; i < ranges.length; i += 1) { - const range = ranges[i]; - transformed += source.slice(c, range.start) + range.toString(nameBySource); - - c = range.end; - } - - transformed += source.slice(c); - exportDeclarations.forEach(d => { - if (d.name) transformed += `\n__exports.${d.as || d.name} = ${d.name};`; - }); - transformed += '\n});\n'; - - // replace trailing ; at the end of commented out lines - transformed = transformed.replace(/(^\/\*import\s+.*\*\/);/gm, '$1'); - - return { externalModules, transformed }; -}; diff --git a/dev/rollup.js b/dev/rollup.js new file mode 100644 index 0000000..82362e5 --- /dev/null +++ b/dev/rollup.js @@ -0,0 +1,402 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import less from 'less'; +import MagicString from 'magic-string'; +import { rollup } from 'rollup'; +import { walk } from 'estree-walker'; +import rollupNodeResolve from '@rollup/plugin-node-resolve'; +import rollupCommonjs from '@rollup/plugin-commonjs'; +import rollupTerser from '@rollup/plugin-terser'; +import rollupCleanup from 'rollup-plugin-cleanup'; +import { getBabelOutputPlugin as rollupBabel } from '@rollup/plugin-babel'; + +const encloseInSingleQuotes = text => { + if (!text) { + return "''"; + } + return "'" + text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '') + "'"; +}; + +export const getImportsExports = ast => { + /** + * List of named variables imported from the module. Used only for `{ x, y, z } from 'abc'` + */ + const imports = {}; + + /** + * List of exported variables of the module + */ + const exports = {}; + + walk(ast, { + enter(node) { + if (node.type === 'ImportDeclaration') { + const moduleId = node.source.value?.toString().replace('@windy/', ''); + const specifiers = node.specifiers.map(s => ({ + name: s.local.name, + start: node.start, + end: node.end, + exportedName: ('imported' in s && s.imported.name) || undefined, + type: s.type, + })); + + if (!moduleId) { + throw new Error('ImportDeclaration has no moduleId!'); + } + + // eg. import '@windy/router'; + if (!specifiers || !specifiers.length) { + specifiers.push({ + name: moduleId, + type: 'ImportDefaultSpecifier', + start: node.start, + end: node.end, + exportedName: undefined, + }); + } + + if (specifiers.some(s => !s.name || !s.type)) { + throw new Error('ImportDeclaration contains incomplete speciefier!'); + } + + for (const specifier of specifiers) { + if (!imports[moduleId]) { + imports[moduleId] = []; + } + imports[moduleId].push(specifier); + } + } + + if (node.type === 'ExportNamedDeclaration') { + if ( + node.declaration && + ['FunctionDeclaration', 'ClassDeclaration', 'VariableDeclaration'].includes( + node.declaration.type, + ) + ) { + const { id: idNode } = + node.declaration.type === 'VariableDeclaration' + ? node.declaration.declarations?.[0] ?? {} + : node.declaration; + + if (idNode.name) { + exports[idNode.name] = { + declaration: { + start: node.declaration.start, + end: node.declaration.end, + }, + start: node.start, + end: node.end, + type: 'PrefixedDeclaration', + }; + } + } + + if (node.specifiers.length) { + for (const n of node.specifiers) { + exports[n.exported.name] = { + declaration: { + name: n.local.name, + }, + start: node.start, + end: node.end, + type: 'SpecifierBlockDeclaration', + }; + } + } + } else if (node.type === 'ExportDefaultDeclaration') { + exports['default'] = { + declaration: { + start: node.declaration.start, + end: node.declaration.end, + }, + start: node.start, + end: node.end, + type: 'DefaultDeclaration', + }; + } else if (node.type === 'ExportAllDeclaration') { + throw new Error(`Unknown export type ${node.type}. Feel free to extend me!`); + } + }, + }); + + return { exports, imports }; +}; + +const getRandomName = () => + `__dep_${Math.random() + .toString(36) + .slice(-10) + .replace(/[^a-zA-Z_]/g, '')}`; + +export const transformCodeToPlugin = (id, meta, code, sourcemaps, pluginContext) => { + const exportVariable = '__exports'; + + const ast = pluginContext.parse(code); + const msCode = new MagicString(code); + const { imports, exports } = getImportsExports(ast); + + const importedIds = Object.keys(imports).map(String); + + // remove all imports from code + for (const importedId of importedIds) { + const declaratedImports = imports[importedId]; + for (const declaratedImport of declaratedImports) { + msCode.remove(declaratedImport.start, declaratedImport.end); + } + } + + msCode.trimLines(); + + // Some modules are imported in different files of same module under deffirent names, this holds all used namespaces + // + // eg for + // import * as _ from '@windy/utils'; import { $ } from '@windy/utils'; import * as utils from '@windy/utils'; etc... + // it holds + // importedIdNamespaces['@windy/utils'] = ['_', 'utils'] + // + // or for imports without namespace + // import { t } from '@windy/trans'; + // it holds + // importedIdNamespaces['@windy/trans'] = ['__dep_someRandomHash'] + const importedIdNamespaces = importedIds.reduce((acc, m) => { + if (!acc[m]) { + acc[m] = []; + } + const hasNamespace = type => + ['ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(type); + const importsWithNamespace = new Set( + imports[m].filter(i => hasNamespace(i.type)).map(i => i.name), + ); + acc[m] = importsWithNamespace.size + ? Array.from(importsWithNamespace) + : // no aliases from default or namespace imports, only { named } imports are presented + // so create some random, it does not matter, just avoid any possible conflict + [getRandomName()]; + return acc; + }, {}); + + // prepend eg: `var joinPath = utils.joinPath;` + for (const importedId of importedIds) { + for (const i of imports[importedId]) { + // only for { named } imports or when exported name is different than local name + if (i.type === 'ImportSpecifier' || (i.exportedName && i.name !== i.exportedName)) { + msCode.prepend( + `var ${i.name} = ${importedIdNamespaces[importedId][0]}.${i.exportedName};\n`, + ); + } + } + } + + // prepend our DI header + const moduleIdsString = importedIds.map(m => `'${m}'`).join(', '); + const moduleNamesString = [ + exportVariable, + ...importedIds.map(mid => importedIdNamespaces[mid][0]), + ].join(', '); + // e.g const utils = u = ut = _; + const moduleAliasesString = importedIds + .map(mid => importedIdNamespaces[mid]) + .filter(a => a.length > 1) + .map(a => { + const [mainAlias, ...otherUsedAliases] = a; + return `var ${otherUsedAliases.join(' = ')} = ${mainAlias};\n`; + }) + .join(''); + + msCode.prepend( + `W.loadPlugin(${JSON.stringify( + meta, + )}, [${moduleIdsString}], function(${moduleNamesString}) {\n${moduleAliasesString}`, + ); + + // append our DI exports + const exportedNames = Object.keys(exports).map(String); + for (const exportedName of exportedNames) { + const exp = exports[exportedName]; + + switch (exp.type) { + case 'PrefixedDeclaration': + // remove `export ` string, keep the rest + msCode.remove(exp.start, exp.declaration.start); + msCode.append(`\n${exportVariable}.${exportedName} = ${exportedName};`); + break; + case 'SpecifierBlockDeclaration': + // remove whole export { ... } block + msCode.remove(exp.start, exp.end); + msCode.append(`\n${exportVariable}.${exportedName} = ${exp.declaration.name};`); + break; + case 'DefaultDeclaration': + // remove whole export default ... block + msCode.remove(exp.start, exp.end); + msCode.append( + `\n${exportVariable}.${exportedName} = ${code.slice( + exp.declaration.start, + exp.declaration.end, + )};`, + ); + break; + } + } + + // html and css is injected by buildPluginsExternal(Html|Css) plugins + let { html, css } = pluginContext.getModuleInfo(id)?.meta || {}; + if (html) { + html = `,\n${encloseInSingleQuotes(html)}`; + } + if (css) { + css = `,\n${encloseInSingleQuotes(css)}`; + if (!html) { + html = ',\nundefined'; + } + } + msCode.append(`\n}${html || ''}${css || ''});`); + + return { + code: msCode.toString(), + map: sourcemaps ? msCode.generateMap({ hires: true }) : undefined, + }; +}; + +export function transformToPlugin(meta) { + let shouldGenerateSourcemaps = false; + let pluginName = ''; + + const processTransformation = (id, code, pluginContext) => { + // all other cases transform to W.define module + return transformCodeToPlugin(id, meta, code, shouldGenerateSourcemaps, pluginContext); + }; + + return { + name: 'transform-to-plugin', + options(options) { + const outputOptions = Array.isArray(options.output) + ? options.output[0] + : options.output ?? {}; + + shouldGenerateSourcemaps = Boolean(outputOptions.sourcemap); + pluginName = outputOptions.name; + + if (!pluginName) { + throw new Error(`Rollup option "output.name" is not defined!`); + } + + return undefined; + }, + renderChunk(code, chunk) { + const id = chunk.facadeModuleId; + if (!id) { + return null; + } + + return processTransformation(id, code, this); + }, + }; +} + +export function buildPluginsHtml() { + return { + name: 'build-plugins-external-html', + transform(code, id) { + // we want to process it only once for each module - entry file check is ideal solution + if (!this.getModuleInfo(id)?.isEntry) { + return; + } + + const htmlFilePath = id.replace(/\.[a-zA-Z]+$/, '.html'); + const htmlContent = fs.existsSync(htmlFilePath) + ? fs.readFileSync(htmlFilePath, { encoding: 'utf8' }) + : null; + if (!htmlContent) { + return; + } + + this.addWatchFile(htmlFilePath); + + // pass data to another rollup steps using meta + return { meta: { html: htmlContent } }; + }, + }; +} + +export function buildPluginsCss() { + return { + name: 'build-plugins-external-css', + async transform(code, id) { + // we want to process it only once for each module - entry file check is ideal solution + if (!this.getModuleInfo(id)?.isEntry) { + return; + } + + const lessFilePath = id.replace(/\.[a-zA-Z]+$/, '.less'); + const lessContent = fs.existsSync(lessFilePath) + ? fs.readFileSync(lessFilePath).toString() + : null; + if (!lessContent) { + return; + } + + const lessResult = await less.render(lessContent, { + math: 'always', + filename: path.resolve(lessFilePath), // make relative import paths inside LESS files valid + }); + + [lessFilePath, ...lessResult.imports].forEach(this.addWatchFile); + if (!lessResult.css) { + return; + } + + // pass data to another rollup steps using meta + return { meta: { css: lessResult.css } }; + }, + }; +} + +export const bundleFile = async rollupOptions => { + const bundle = await rollup(rollupOptions); + const { output } = await bundle.generate(rollupOptions.output); + const code = output[0].code; + const map = output[0].map; + + return { code, map, watchFiles: bundle.watchFiles }; +}; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const builder = async function (id, srcDir, meta) { + const bundled = await bundleFile({ + output: { + dir: path.join(__dirname, 'dist'), + format: 'es', + name: id, + }, + input: path.join(srcDir, 'plugin.js'), + external: moduleId => moduleId.startsWith('@windy/') || moduleId.startsWith('@plugins/'), + plugins: [ + buildPluginsHtml(), + buildPluginsCss(), + rollupNodeResolve({ + mainFields: ['module', 'jsnext:main', 'main'], + browser: true, + preferBuiltins: false, + }), + rollupCommonjs(), + rollupCleanup({ comments: 'none' }), + rollupBabel({ + presets: [['@babel/preset-env', { targets: 'ie 11', modules: false }]], + allowAllFormats: true, + compact: false, + }), + transformToPlugin(meta), + rollupTerser({ + mangle: true, + ecma: 5, + compress: { ecma: 5 }, + }), + ], + }); + return bundled; +}; diff --git a/dev/shimport.js b/dev/shimport.js deleted file mode 100644 index e16b9c3..0000000 --- a/dev/shimport.js +++ /dev/null @@ -1,740 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -function get_alias(specifiers, name) { - let i = specifiers.length; - while (i--) { - if (specifiers[i].name === name) return specifiers[i].as; - } -} - -function importDecl(str, start, end, specifiers, source) { - const name = get_alias(specifiers, '*') || get_alias(specifiers, 'default'); - - return { - start, - end, - source, - name, - specifiers, - toString() { - return `/*${str.slice(start, end)}*/`; - }, - }; -} - -function exportDefaultDeclaration(str, start, end) { - const match = /^\s*(?:(class)(\s+extends|\s*{)|(function)\s*\()/.exec(str.slice(end)); - - if (match) { - // anonymous class declaration - end += match[0].length; - - const name = '__default_export'; - - return { - start, - end, - name, - as: 'default', - toString() { - return match[1] ? `class ${name}${match[2]}` : `function ${name}(`; - }, - }; - } - - return { - start, - end, - toString() { - return `__exports.default =`; - }, - }; -} - -function exportSpecifiersDeclaration(str, start, specifiersStart, specifiersEnd, end, source) { - const specifiers = processSpecifiers(str.slice(specifiersStart + 1, specifiersEnd - 1).trim()); - - return { - start, - end, - source, - toString(nameBySource) { - const name = source && nameBySource.get(source); - - return ( - specifiers - .map(s => { - return `__exports.${s.as} = ${name ? `${name}.${s.name}` : s.name}; `; - }) - .join('') + `/*${str.slice(start, end)}*/` - ); - }, - }; -} - -function exportDecl(str, start, c) { - const end = c; - - while (str[c] && /\S/.test(str[c])) c += 1; - while (str[c] && !/\S/.test(str[c])) c += 1; - - const nameStart = c; - while (str[c] && !punctuatorChars.test(str[c]) && !isWhitespace(str[c])) c += 1; - const nameEnd = c; - - const name = str.slice(nameStart, nameEnd); - - return { - start, - end, - name, - toString() { - return ''; - }, - }; -} - -function exportStarDeclaration(str, start, end, source) { - return { - start, - end, - source, - toString(nameBySource) { - return `Object.assign(__exports, ${nameBySource.get(source)}); /*${str.slice( - start, - end, - )}*/`; - }, - }; -} - -const keywords = /\b(case|default|delete|do|else|in|instanceof|new|return|throw|typeof|void)\s*$/; -const punctuators = /(^|\{|\(|\[\.|;|,|<|>|<=|>=|==|!=|===|!==|\+|-|\*\%|<<|>>|>>>|&|\||\^|!|~|&&|\|\||\?|:|=|\+=|-=|\*=|%=|<<=|>>=|>>>=|&=|\|=|\^=|\/=|\/)\s*$/; -const ambiguous = /(\}|\)|\+\+|--)\s*$/; - -const punctuatorChars = /[{}()[.;,<>=+\-*%&|\^!~?:/]/; -const keywordChars = /[a-zA-Z_$0-9]/; - -const whitespace_obj = { - ' ': 1, - '\t': 1, - '\n': 1, - '\r': 1, - '\f': 1, - '\v': 1, - '\u00A0': 1, - '\u2028': 1, - '\u2029': 1, -}; - -function isWhitespace(char) { - // this is faster than testing a regex - return char in whitespace_obj; -} - -function isQuote(char) { - return char === "'" || char === '"'; -} - -const namespaceImport = /^\*\s+as\s+(\S+)$/; -const defaultAndStarImport = /(\w+)\s*,\s*\*\s*as\s*(\S+)$/; -const defaultAndNamedImport = /(\w+)\s*,\s*{(.+)}$/; - -function processImportSpecifiers(str) { - let match = namespaceImport.exec(str); - if (match) { - return [{ name: '*', as: match[1] }]; - } - - match = defaultAndStarImport.exec(str); - if (match) { - return [ - { name: 'default', as: match[1] }, - { name: '*', as: match[2] }, - ]; - } - - match = defaultAndNamedImport.exec(str); - if (match) { - return [{ name: 'default', as: match[1] }].concat(processSpecifiers(match[2].trim())); - } - - if (str[0] === '{') return processSpecifiers(str.slice(1, -1).trim()); - - if (str) return [{ name: 'default', as: str }]; - - return []; -} - -function processSpecifiers(str) { - return str - ? str.split(',').map(part => { - const [name, , as] = part.trim().split(/[^\S]+/); - return { name, as: as || name }; - }) - : []; -} - -function getImportDeclaration(str, i) { - const start = i; - - const specifierStart = (i += 6); - while (str[i] && isWhitespace(str[i])) i += 1; - while (str[i] && !isQuote(str[i])) i += 1; - const specifierEnd = i; - - const sourceStart = (i += 1); - while (str[i] && !isQuote(str[i])) i += 1; - const sourceEnd = i++; - - return importDecl( - str, - start, - i, - processImportSpecifiers( - str - .slice(specifierStart, specifierEnd) - .replace(/from\s*$/, '') - .trim(), - ), - str.slice(sourceStart, sourceEnd), - ); -} - -function getImportStatement(i) { - return { - start: i, - end: i + 6, - toString() { - return '__import'; - }, - }; -} - -const importMetaUrlPattern = /^import\s*\.\s*meta\s*\.\s*url/; - -function getImportMetaUrl(str, start, id) { - const match = importMetaUrlPattern.exec(str.slice(start)); - if (match) { - return { - start, - end: start + match[0].length, - toString() { - return JSON.stringify('' + id); - }, - }; - } -} - -function getExportDeclaration(str, i) { - const start = i; - - i += 6; - while (str[i] && isWhitespace(str[i])) i += 1; - - const declarationStart = i; - - if (str[i] === '{') { - while (str[i] !== '}') i += 1; - i += 1; - - const specifiersEnd = i; - - let source = null; - - while (isWhitespace(str[i])) i += 1; - if (/^from[\s\n'"]/.test(str.slice(i, i + 5))) { - i += 4; - while (isWhitespace(str[i])) i += 1; - - while (str[i] && !isQuote(str[i])) i += 1; - const sourceStart = (i += 1); - while (str[i] && !isQuote(str[i])) i += 1; - - source = str.slice(sourceStart, i); - i += 1; - } - - return exportSpecifiersDeclaration(str, start, declarationStart, specifiersEnd, i, source); - } - - if (str[i] === '*') { - i += 1; - while (isWhitespace(str[i])) i += 1; - i += 4; - while (str[i] && !isQuote(str[i])) i += 1; - - const sourceStart = (i += 1); - while (str[i] && !isQuote(str[i])) i += 1; - const sourceEnd = i++; - - return exportStarDeclaration(str, start, i, str.slice(sourceStart, sourceEnd)); - } - - if (/^default\b/.test(str.slice(i, i + 8))) { - return exportDefaultDeclaration(str, start, declarationStart + 7); - } - - return exportDecl(str, start, declarationStart); -} - -function find(str, id) { - let escapedFrom; - let regexEnabled = true; - let pfixOp = false; - - const stack = []; - - let lsci = -1; // last significant character index - const lsc = () => str[lsci]; - - var parenMatches = {}; - var openingParenPositions = {}; - var parenDepth = 0; - - const importDeclarations = []; - const importStatements = []; - const importMetaUrls = []; - const exportDeclarations = []; - - function tokenClosesExpression() { - if (lsc() === ')') { - var c = parenMatches[lsci]; - while (isWhitespace(str[c - 1])) { - c -= 1; - } - - // if parenthesized expression is immediately preceded by `if`/`while`, it's not closing an expression - return !/(if|while)$/.test(str.slice(c - 5, c)); - } - - // TODO handle }, ++ and -- tokens immediately followed by / character - return true; - } - - const base = { - pattern: /(?:(\()|(\))|({)|(})|(")|(')|(\/\/)|(\/\*)|(\/)|(`)|(import)|(export)|(\+\+|--))/g, - - handlers: [ - // ( - i => { - lsci = i; - openingParenPositions[parenDepth++] = i; - }, - - // ) - i => { - lsci = i; - parenMatches[i] = openingParenPositions[--parenDepth]; - }, - - // { - i => { - lsci = i; - stack.push(base); - }, - - // } - i => { - lsci = i; - return stack.pop(); - }, - - // " - i => { - stack.push(base); - return double_quoted; - }, - - // ' - i => { - stack.push(base); - return single_quoted; - }, - - // // - i => line_comment, - - // /* - i => block_comment, - - // / - i => { - // could be start of regex literal OR division punctuator. Solution via - // http://stackoverflow.com/questions/5519596/when-parsing-javascript-what-determines-the-meaning-of-a-slash/27120110#27120110 - - var b = i; - while (b > 0 && isWhitespace(str[b - 1])) { - b -= 1; - } - - if (b > 0) { - var a = b; - - if (punctuatorChars.test(str[a - 1])) { - while (a > 0 && punctuatorChars.test(str[a - 1])) { - a -= 1; - } - } else { - while (a > 0 && keywordChars.test(str[a - 1])) { - a -= 1; - } - } - - var token = str.slice(a, b); - - regexEnabled = token - ? keywords.test(token) || - punctuators.test(token) || - (ambiguous.test(token) && !tokenClosesExpression()) - : false; - } else { - regexEnabled = true; - } - - return slash; - }, - - // ` - i => template_string, - - // import - i => { - if (i === 0 || isWhitespace(str[i - 1]) || punctuatorChars.test(str[i - 1])) { - let j = i + 6; - let char; - - do { - char = str[j++]; - } while (isWhitespace(char)); - - const hasWhitespace = j > i + 7; - - if (/^['"{*]$/.test(char) || (hasWhitespace && /^[a-zA-Z_$]$/.test(char))) { - const d = getImportDeclaration(str, i); - importDeclarations.push(d); - p = d.end; - } else if (char === '(') { - const s = getImportStatement(i); - importStatements.push(s); - p = s.end; - } else if (char === '.') { - const u = getImportMetaUrl(str, i, id); - if (u) { - importMetaUrls.push(u); - p = u.end; - } - } - } - }, - - // export - i => { - if (i === 0 || isWhitespace(str[i - 1]) || punctuatorChars.test(str[i - 1])) { - if (/export[\s\n{]/.test(str.slice(i, i + 7))) { - const d = getExportDeclaration(str, i); - exportDeclarations.push(d); - p = d.end; - } - } - }, - - // ++/-- - i => { - pfixOp = !pfixOp && str[i - 1] === '+'; - }, - ], - }; - - const slash = { - pattern: /(?:(\[)|(\\)|(.))/g, - - handlers: [ - // [ - i => (regexEnabled ? regex_character : base), - - // \\ - i => ((escapedFrom = regex), escaped), - - // anything else - i => (regexEnabled && !pfixOp ? regex : base), - ], - }; - - const regex = { - pattern: /(?:(\[)|(\\)|(\/))/g, - - handlers: [ - // [ - () => regex_character, - - // \\ - () => ((escapedFrom = regex), escaped), - - // / - () => base, - ], - }; - - const regex_character = { - pattern: /(?:(\])|(\\))/g, - - handlers: [ - // ] - () => regex, - - // \\ - () => ((escapedFrom = regex_character), escaped), - ], - }; - - const double_quoted = { - pattern: /(?:(\\)|("))/g, - - handlers: [ - // \\ - () => ((escapedFrom = double_quoted), escaped), - - // " - () => stack.pop(), - ], - }; - - const single_quoted = { - pattern: /(?:(\\)|('))/g, - - handlers: [ - // \\ - () => ((escapedFrom = single_quoted), escaped), - - // ' - () => stack.pop(), - ], - }; - - const escaped = { - pattern: /(.)/g, - - handlers: [() => escapedFrom], - }; - - const template_string = { - pattern: /(?:(\${)|(\\)|(`))/g, - - handlers: [ - // ${ - () => { - stack.push(template_string); - return base; - }, - - // \\ - () => ((escapedFrom = template_string), escaped), - - // ` - () => base, - ], - }; - - const line_comment = { - pattern: /((?:\n|$))/g, - - handlers: [ - // \n - () => base, - ], - }; - - const block_comment = { - pattern: /(\*\/)/g, - - handlers: [ - // \n - () => base, - ], - }; - - let state = base; - - let p = 0; - - while (p < str.length) { - state.pattern.lastIndex = p; - const match = state.pattern.exec(str); - - if (!match) { - if (stack.length > 0 || state !== base) { - throw new Error(`Unexpected end of file`); - } - - break; - } - - p = match.index + match[0].length; - - for (let j = 1; j < match.length; j += 1) { - if (match[j]) { - state = state.handlers[j - 1](match.index) || state; - break; - } - } - } - - return [importDeclarations, importStatements, importMetaUrls, exportDeclarations]; -} - -function transform(source, id, sourcePath, html, css) { - const [importDeclarations, importStatements, importMetaUrls, exportDeclarations] = find( - source, - id, - ); - - const nameBySource = new Map(); - - importDeclarations.forEach(d => { - if (nameBySource.has(d.source)) return; - nameBySource.set(d.source, d.name || `__dep_${nameBySource.size}`); - }); - - let moduleHasNamedExport = false; - let moduleHaDefaultExport = false; - - exportDeclarations.forEach(d => { - if (!d.name) { - moduleHaDefaultExport = true; - } else { - moduleHasNamedExport = true; - } - - if (!d.source) return; - if (nameBySource.has(d.source)) return; - nameBySource.set(d.source, d.name || `__dep_${nameBySource.size}`); - }); - - if (moduleHaDefaultExport && moduleHasNamedExport) { - console.log(exportDeclarations); - consola.error( - `es2Wdefine detected combination of named and default exports in one module: ${gray( - id, - )}`, - ); - } - - const deps = Array.from(nameBySource.keys()) - .map(s => `'${s.replace(/^@windy\//, '')}'`) - .join(', '); - - const names = ['__exports'].concat(Array.from(nameBySource.values())).join(', '); - - const hoisted = []; - - importDeclarations.forEach(decl => { - const name = nameBySource.get(decl.source); - let moduleHasNamedImport = false; - let moduleHaDefaultImport = false; - - decl.specifiers - .sort((a, b) => { - if (a.name === 'default') { - return 1; - } - if (b.name === 'default') { - return -1; - } - }) - .forEach(s => { - if (s.name === 'default') { - moduleHaDefaultImport = true; - } else { - moduleHasNamedImport = true; - } - - if (s.name !== '*') { - /** - * Original version combining default & named exports - - const assignment = - s.name === 'default' && s.as === name - ? `${s.as} = ${name}.default; ` - : `var ${s.as} = ${name}.${s.name}; `; - - hoisted.push(assignment); - - */ - - if (s.name !== 'default') { - hoisted.push(`var ${s.as} = ${name}.${s.name}; `); - } - } - }); - if (moduleHaDefaultImport && moduleHasNamedImport) { - consola.error( - `es2Wdefine detected combination of named and default import in one module: ${gray( - id, - )}`, - ); - } - }); - - let transformed = `W.define('${id}',\n[${deps}], function(${names}){ \n\n${hoisted.join('\n') + - '\n\n'}`; - - const ranges = [ - ...importDeclarations, - ...importStatements, - ...importMetaUrls, - ...exportDeclarations, - ].sort((a, b) => a.start - b.start); - - let c = 0; - - for (let i = 0; i < ranges.length; i += 1) { - const range = ranges[i]; - transformed += source.slice(c, range.start) + range.toString(nameBySource); - - c = range.end; - } - - transformed += source.slice(c); - - exportDeclarations.forEach(d => { - if (d.name) transformed += `\n__exports.${d.as || d.name} = ${d.name};`; - }); - - /** - * Appned css, html to the define - */ - if (html) { - html = `,\n/*! */\n${_q(html, true)}`; - } - - if (css) { - css = `,\n/*! */\n${_q(css)}`; - if (!html) { - html = ',\n/*! */\nfalse'; - } - } - - transformed += `\n}${html || ''}${css || ''});\n//# XXXXsourceURL=${sourcePath || id}`; - - return transformed; -} - -/** - * Ensloses the string in single quotes - */ -const _q = (s, r) => { - if (!s) { - return "''"; - } - s = "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"; - return r && s.indexOf('\n') !== -1 ? s.replace(/\n/g, '\\n') : s; -}; - -var VERSION = '0.0.12'; - -exports.transform = transform; -exports.find = find; -exports.VERSION = VERSION; diff --git a/dev/utils.js b/dev/utils.js deleted file mode 100644 index ba63fe0..0000000 --- a/dev/utils.js +++ /dev/null @@ -1,129 +0,0 @@ -const ucfirst = require('ucfirst'), - prompts = require('prompts'), - { yellow } = require('colorette'), - fs = require('fs-extra'), - { join } = require('path'); - -const mjs2js = require('./mjs2js.js'); - -// -// Funny start up prompt -// -exports.prompt = async () => { - let dir = 'src'; - - const list = fs.readdirSync(join(__dirname, '..', 'examples')).filter(d => /\d\d-/.test(d)); - - console.log(`\nSelect which example you want to test:\n`); - - list.map((d, i) => - console.log(` ${yellow(i + 1)}) ${ucfirst(d.replace(/^\d\d-/, '').replace(/-/g, ' '))}`), - ); - - console.log( - `\n ${yellow(0)}) F***K OFF with examples. I am pro. I want to develop ${yellow( - 'my own plugin', - )}.\n`, - ); - - let { value } = await prompts({ - type: 'number', - name: 'value', - message: `Which example you want to launch? (press 0 - ${list.length}):`, - validate: value => (value >= 0 && value < list.length + 1 ? true : false), - }); - - if (value > 0) { - dir = join('examples', list[value - 1]); - } else if (value === 0) { - console.log(`---------------------------------------------------- -Please change ${yellow('package.json')} now: - - ${yellow('name')}: Must contain name of your plugin in a form windy-plugin-AnyName - ${yellow('description')}: Should be description of what your plugin does - ${yellow('author')}: Should contain your name - ${yellow('repository')}: Should be actual link to your hosting repo - -Also ${yellow('./README.md')} should contain some info about your plugin if you wish - -For faster work use directlly ${yellow('npm run start-dev')} to skip this prompt - -After you will be done use ${yellow('npm publish')} to publish your plugin. ------------------------------------------------------`); - } - - return dir; -}; - -// -// Finally creates W.loadPlugin code -// -exports.stringifyPlugin = (opts, html, css, js) => { - const _q = (s, r) => { - if (!s) { - return "''"; - } - s = "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"; - return r && s.indexOf('\n') !== -1 ? s.replace(/\n/g, '\\n') : s; - }; - - return ` -/** - * This is main plugin loading function - * Feel free to write your own compiler - */ -W.loadPlugin( - -/* Mounting options */ -${JSON.stringify(opts, null, 2)}, - -/* HTML */ -${_q(html, 1)}, - -/* CSS */ -${_q(css)}, - -/* Constructor */ -function() {\n\n${js}\n});`; -}; - -/** - * Parses and builds external .mjs files - * @param {Object} internalModules Hask of alredy loade internal modules - * @param {string} module in a form './filename.mjs' - * @return {string} new module name - */ -exports.externalMjs = async (src, internalModules, module, name) => { - const base = module.replace(/\.\/(\S+)\.mjs/, '$1'); - - if (!base) { - throw new Error( - 'Unable to import module. Windy plugin compiler is primitive and' + - ' supports only "@windy/name", or "./filename.mjs" modules', - ); - } - - const moduleId = `${name}/${base}`; - - if (module in internalModules) { - // Module already loaded - - return moduleId; - } else { - const file = join(src, `${base}.mjs`); - const { externalModules, transformed } = await mjs2js(file, moduleId, name); - - internalModules[module] = transformed; - - for (const internal of externalModules) { - // - // Here circular dep can occur - //// - if (!internalModules[internal]) { - await exports.externalMjs(src, internalModules, internal, name); - } - } - - return moduleId; - } -}; diff --git a/package.json b/package.json index 88d947e..50540f5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "windy-plugin-examples", + "type": "module", "version": "0.5.0", "description": "Windy plugin system enables anyone, with basic knowledge of Javascript to enhance Windy with new functionality (default desc).", "main": "dist/plugin.js", @@ -16,23 +17,29 @@ "author": "Windyty, S.E.", "license": "MIT", "dependencies": { - "@babel/core": "^7.1.2", - "@babel/preset-env": "^7.1.0", + "@babel/preset-env": "^7.21.5", + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", "chokidar": "^2.0.4", "colorette": "^1.0.6", "commander": "^2.19.0", "consola": "^1.4.4", "decache": "^4.4.0", + "estree-walker": "^3.0.3", "express": "^4.16.4", "fs-extra": "^7.0.0", "less": "^3.8.1", + "magic-string": "^0.30.0", "prompts": "^1.1.1", - "riot-compiler": "^3.5.1", + "rollup": "^3.22.0", + "rollup-plugin-cleanup": "^3.2.1", "ucfirst": "^1.0.0" }, "devDependencies": { - "eslint": "^8.1.0", - "eslint-plugin-html": "^6.2.0", - "prettier": "^2.4.1" + "eslint": "^8.40.0", + "eslint-plugin-html": "^7.1.0", + "prettier": "^2.8.8" } } diff --git a/test/prepublish.js b/test/prepublish.js index 7c92619..ecb700f 100644 --- a/test/prepublish.js +++ b/test/prepublish.js @@ -1,36 +1,38 @@ -const fs = require('fs-extra'), - assert = require('assert'), - path = require('path'); +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import assert from 'node:assert'; -const { name, author, repository, description } = require(path.join( - __dirname, - '..', - 'package.json' -)); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const { name, author, repository, description } = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'package.json')), +); assert( name !== 'windy-plugin-example', - 'Please modify name in your package.json, it is still default name \u0007' + 'Please modify name in your package.json, it is still default name \u0007', ); assert( /windy-plugin-\S+/.test(name), - 'Name of your plugin MUST have form windy-plugin-XXXX. Please modify name in your package.json. \u0007' + 'Name of your plugin MUST have form windy-plugin-XXXX. Please modify name in your package.json. \u0007', ); assert( !/default desc/.test(description), - 'Please modify description in your package.json, it is still default description \u0007' + 'Please modify description in your package.json, it is still default description \u0007', ); assert( author !== 'Windyty, S.E.', - 'Please modify author in your package.json, it is still default author \u0007' + 'Please modify author in your package.json, it is still default author \u0007', ); assert( repository && !/windycom/.test(repository.url), - 'Please modify repository in your package.json, it is still default repository \u0007' + 'Please modify repository in your package.json, it is still default repository \u0007', ); const readme = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8'); @@ -39,5 +41,5 @@ assert(readme, 'README.md file missing\u0007'); assert( !/This is default readme/.test(readme), - 'The README.md file is still default, please delete it and put there info about your plugin \u0007' + 'The README.md file is still default, please delete it and put there info about your plugin \u0007', );