UNPKG

nadesiko3

Version:
718 lines (676 loc) 27.5 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ // deno-lint-ignore-file no-explicit-any /** * コマンドライン版のなでしこ3をモジュールとして定義 * 実際には cnako3.mjs から読み込まれる */ import fs from 'node:fs' import fse from 'fs-extra' import { exec } from 'node:child_process' import path from 'node:path' import { NakoCompiler, LoaderTool, newCompilerOptions } from '../core/src/nako3.mjs' import { NakoImportError } from '../core/src/nako_errors.mjs' import { Ast, CompilerOptions } from '../core/src/nako_types.mjs' import { NakoGlobal } from '../core/src/nako_global.mjs' import nakoVersion from './nako_version.mjs' import PluginNode from './plugin_node.mjs' import app from './commander_ja.mjs' import fetch from 'node-fetch' import { NakoGenOptions } from '../core/src/nako_gen.mjs' import { getEnv, isWindows, getCommandLineArgs, exit } from './deno_wrapper.mjs' // __dirname のために import url from 'node:url' const __filename = url.fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) /** コマンドラインアクション */ interface CNako3ArgOptions { warn: boolean debug: boolean compile: any | boolean test: any | boolean one_liner: any | boolean trace: any | boolean run: any | boolean repl: any | boolean source: any | string mainfile: any | string man: string browsers: boolean ast: boolean lex: boolean } interface CNako3Options { nostd: boolean } /** CNako3 */ export class CNako3 extends NakoCompiler { debug: boolean version: string constructor (opts:CNako3Options = { nostd: false }) { super({ useBasicPlugin: !opts.nostd }) this.debug = false this.filename = 'main.nako3' this.version = nakoVersion.version if (!opts.nostd) { this.addPluginFromFile('PluginNode', PluginNode, true) } // 必要な定数を設定 this.addListener('beforeRun', (g: NakoGlobal) => { g.__varslist[0].set('ナデシコ種類', 'cnako3') g.__varslist[0].set('ナデシコバージョン', this.version) }) } // CNAKO3で使えるコマンドを登録する registerCommands () { // コマンドライン引数を得る const args: string[] = getCommandLineArgs() // コマンド引数がないならば、ヘルプを表示(-hはcommandarにデフォルト用意されている) if (args.length <= 2) { args.push('-h') } const verInfo = `v${nakoVersion.version}` // commanderを使って引数を解析する app .title('日本語プログラミング言語「なでしこ」' + verInfo) .version(verInfo, '-v, --version') .usage('[オプション] 入力ファイル.nako3') .option('-h, --help', 'コマンドの使い方を表示') .option('-w, --warn', '警告を表示する') .option('-d, --debug', 'デバッグモードの指定') .option('-D, --trace', '詳細デバッグモードの指定') .option('-c, --compile', 'コンパイルモードの指定') .option('-t, --test', 'コンパイルモードの指定 (テスト用コードを出力)') .option('-r, --run', 'コンパイルモードでも実行する') .option('-e, --eval [src]', '直接プログラムを実行するワンライナーモード') .option('-o, --output', '出力ファイル名の指定') .option('-s, --silent', 'サイレントモードの指定') .option('-l, --repl', '対話シェル(REPL)の実行') .option('-b, --browsers', '対応機器/Webブラウザを表示する') .option('-m, --man [command]', 'マニュアルを表示する') .option('-p, --speed', 'スピード優先モードの指定') .option('-A, --ast', '構文解析した結果をASTで出力する') .option('-X, --lex', '字句解析した結果をJSONで出力する') // .option('-h, --help', '使い方を表示する') // .option('-v, --version', 'バージョンを表示する') .parse(args) return app } /** コマンドライン引数を解析 */ checkArguments (): CNako3ArgOptions { const app: any = this.registerCommands() let logLevel = 'error' if (app.trace) { logLevel = 'trace' } else if (app.debug) { logLevel = 'debug' } else if (app.warn) { logLevel = 'warn' } this.getLogger().addListener(logLevel, ({ nodeConsole }) => { console.log(nodeConsole) }) const args: any = { compile: app.compile || false, run: app.run || false, source: app.eval || '', man: app.man || '', one_liner: app.eval || false, debug: this.debug || false, trace: app.trace, warn: app.warn, repl: app.repl || false, test: app.test || false, browsers: app.browsers || false, speed: app.speed || false, ast: app.ast || false, lex: app.lex || false } args.mainfile = app.args[0] args.output = app.output // todo: ESModule 対応の '.mjs' のコードを吐くように修正 #1217 const ext = '.mjs' if (/\.(nako|nako3|txt|bak)$/.test(args.mainfile)) { if (!args.output) { if (args.test) { args.output = args.mainfile.replace(/\.(nako|nako3)$/, '.spec' + ext) } else { args.output = args.mainfile.replace(/\.(nako|nako3)$/, ext) } } } else { if (!args.output) { if (args.test) { args.output = args.mainfile + '.spec' + ext } else { args.output = args.mainfile + ext } } args.mainfile += '.nako3' } return args } // 実行する async execCommand () { // コマンドを解析 const opt: CNako3ArgOptions = this.checkArguments() // 使い方の表示か? if (opt.man) { this.cnakoMan(opt.man) return } // 対応ブラウザを表示する if (opt.browsers) { this.cnakoBrowsers() return } // REPLを実行する if (opt.repl) { this.cnakoRepl() return } // ワンライナーで実行する if (opt.one_liner) { this.cnakoOneLiner(opt) return } // メインプログラムを読み込む this.filename = opt.mainfile const src = fs.readFileSync(opt.mainfile, 'utf-8') if (opt.compile) { await this.nakoCompile(opt, src, false) return } // 字句解析の結果をJSONで出力 if (opt.lex) { const lex = this.lex(src, opt.mainfile) console.log(this.outputJSON(lex, 0)) return } // ASTを出力する if (opt.ast) { try { await this.loadDependencies(src, opt.mainfile, '') } catch (err: any) { if (this.numFailures > 0) { this.logger.error(err) exit(1) } } this.outputAST(opt, src) return } // テストを実行する if (opt.test) { try { await this.loadDependencies(src, opt.mainfile, '') this.test(src, opt.mainfile) return } catch (e: any) { if (this.numFailures > 0) { this.logger.error(e) exit(1) } } } // ファイルを読んで実行する try { // コンパイルと実行を行うメソッド const g = await this.runAsync(src, opt.mainfile) return g } catch (e: any) { // 文法エラーなどがあった場合 if (opt.debug || opt.trace) { throw e } } } /** * コンパイルモードの場合 */ async nakoCompile (opt: any, src: string, isTest: boolean) { // 依存ライブラリなどを読み込む await this.loadDependencies(src, this.filename, '') // JSにコンパイル const genOpt = new NakoGenOptions( isTest, ['plugin_node.mjs'], 'self.__setSysVar(\'ナデシコ種類\', \'cnako3\');' ) const jscode = this.compileStandalone(src, this.filename, genOpt) console.log(opt.output) fs.writeFileSync(opt.output, jscode, 'utf-8') // 実行に必要なファイルをコピー const nakoRuntime = __dirname const outRuntime = path.join(path.dirname(opt.output), 'nako3runtime') if (!fs.existsSync(outRuntime)) { fs.mkdirSync(outRuntime) } // from ./src for (const mod of ['nako_version.mjs', 'plugin_node.mjs', 'deno_wrapper.mjs']) { fs.copyFileSync(path.join(nakoRuntime, mod), path.join(outRuntime, mod)) } // from nadesiko3core/src const srcDir = path.join(__dirname, '..', 'core', 'src') const baseFiles = ['nako_errors.mjs', 'nako_core_version.mjs', 'plugin_system.mjs', 'plugin_math.mjs', 'plugin_promise.mjs', 'plugin_test.mjs', 'plugin_csv.mjs', 'nako_csv.mjs'] for (const mod of baseFiles) { fs.copyFileSync(path.join(srcDir, mod), path.join(outRuntime, mod)) } // or 以下のコピーだと依存ファイルがコピーされない package.jsonを見てコピーする必要がある const orgModule = path.join(__dirname, '..', 'node_modules') const dirNodeModules = path.join(path.dirname(opt.output), 'node_modules') const modlist = ['fs-extra', 'iconv-lite', 'opener', 'node-fetch', 'shell-quote'] const copied: { [key: string]: boolean } = {} // 再帰的に必要なモジュールをコピーする const copyModule = (mod: string) => { if (copied[mod]) { return } copied[mod] = true // ライブラリ自身をコピー fse.copySync(path.join(orgModule, mod), path.join(dirNodeModules, mod)) // 依存ライブラリをコピー const packageFile = path.join(orgModule, mod, 'package.json') const jsonStr = fs.readFileSync(packageFile, 'utf-8') const jsonData = JSON.parse(jsonStr) // サブモジュールをコピー for (const smod in jsonData.dependencies) { copyModule(smod) } } for (const mod of modlist) { copyModule(mod) } if (opt.run) { exec(`node ${opt.output}`, function (err, stdout, stderr) { if (err) { console.log('[ERROR]', stderr) } console.log(stdout) }) } } // ワンライナーの場合 async cnakoOneLiner (opt: any) { const org = opt.source try { if (opt.source.indexOf('表示') < 0) { opt.source = '' + opt.source + 'を表示。' } await this.runAsync(opt.source, 'main.nako3') } catch (e) { // エラーになったら元のワンライナーで再挑戦 try { if (opt.source !== org) { await this.runAsync(org, 'main.nako3') } else { throw e } } catch (e: any) { if (this.debug) { throw e } else { console.error(e.message) } } } } /** * JSONを出力 */ outputJSON (ast: any, level: number): string { const makeIndent = (level: number) => { let s = '' for (let i = 0; i < level; i++) { s += ' ' } return s } const trim = (s: string) => { return s.replace(/(^\s+|\s+$)/g, '') } if (typeof (ast) === 'string') { return makeIndent(level) + '"' + ast + '"' } if (typeof (ast) === 'number') { return makeIndent(level) + ast } if (ast instanceof Array) { const s = makeIndent(level) + '[\n' const sa: string[] = [] ast.forEach((a: Ast) => { sa.push(this.outputJSON(a, level + 1)) }) return s + sa.join(',\n') + '\n' + makeIndent(level) + ']' } if (ast instanceof Object) { const s = makeIndent(level) + '{\n' const sa = [] for (const key in ast) { const sv = trim(this.outputJSON((ast as any)[key], level + 1)) const so = makeIndent(level + 1) + '"' + key + '": ' + sv sa.push(so) } return s + sa.join(',\n') + '\n' + makeIndent(level) + '}' } return makeIndent(level) + ast } /** * ASTを出力 */ outputAST (opt: any, src: string) { const ast = this.parse(src, opt.mainfile) console.log(this.outputJSON(ast, 0)) } // REPL(対話実行環境)の場合 async cnakoRepl () { const fname = path.join(__dirname, 'repl.nako3') const src = fs.readFileSync(fname, 'utf-8') await this.runAsync(src, 'main.nako3') } // マニュアルを表示する cnakoMan (command: string) { try { const pathCommands = path.join(__dirname, '../release/command_cnako3.json') const commands = JSON.parse(fs.readFileSync(pathCommands, 'utf-8')) const data = commands[command] for (const key in data) { console.log(`${key}: ${data[key]}`) } } catch (e: any) { if (e.code === 'MODULE_NOT_FOUND') { console.log('コマンド一覧がないため、マニュアルを表示できません。以下のコマンドでコマンド一覧を生成してください。\n$ npm run build') } else { throw e } } } // 対応機器/Webブラウザを表示する cnakoBrowsers () { const fileMD = path.resolve(__dirname, '../doc', 'browsers.md') console.log(fs.readFileSync(fileMD, 'utf-8')) } // (js|nako3) loader getLoaderTools () { /** @type {string[]} */ const log: string[] = [] const tools: LoaderTool = { resolvePath: (name: string, token: any, fromFile: string): {filePath: string, type: string} => { // 最初に拡張子があるかどうかをチェック // JSプラグインか? if (/\.(js|mjs)(\.txt)?$/.test(name)) { const jspath = CNako3.findJSPluginFile(name, fromFile, __dirname, log) if (jspath === '') { throw new NakoImportError(`JSプラグイン『${name}』が見つかりません。コマンドラインで『npm install ${name}』を実行してみてください。以下のパスを検索しました。\n${log.join('\n')}`, token.file, token.line) } return { filePath: jspath, type: 'js' } } // なでしこプラグインか? if (/\.(nako3|nako)(\.txt)?$/.test(name)) { // ファイルかHTTPか if (name.startsWith('http://') || name.startsWith('https://')) { return { filePath: name, type: 'nako3' } } if (path.isAbsolute(name)) { return { filePath: path.resolve(name), type: 'nako3' } } else { // filename が undefined のとき token.file が undefined になる。 if (token.file === undefined) { throw new Error('ファイル名を指定してください。') } const dir = path.dirname(fromFile) return { filePath: path.resolve(path.join(dir, name)), type: 'nako3' } } } // 拡張子がない、あるいは、(.js|.mjs|.nako3|.nako)以外はJSモジュールと見なす const jspath2 = CNako3.findJSPluginFile(name, fromFile, __dirname, log) if (jspath2 === '') { throw new NakoImportError(`JSプラグイン『${name}』が見つかりません。コマンドラインで『npm install ${name}』を実行してみてください。以下のパスを検索しました。\n${log.join('\n')}`, token.file, token.line) } return { filePath: jspath2, type: 'js' } }, readNako3: (name, token) => { const loader:any = { task: null } // ファイルかHTTPか if (name.startsWith('http://') || name.startsWith('https://')) { // Webのファイルを非同期で読み込む loader.task = (async () => { const res = await fetch(name) if (!res.ok) { throw new NakoImportError(`『${name}』からのダウンロードに失敗しました: ${res.status} ${res.statusText}`, token.file, token.line) } return await res.text() })() } else { // ファイルを非同期で読み込む // ファイルチェックだけ先に実行 if (!fs.existsSync(name)) { throw new NakoImportError(`ファイル ${name} が存在しません。`, token.file, token.line) } loader.task = (new Promise((resolve, reject) => { fs.readFile(name, { encoding: 'utf-8' }, (err, data) => { if (err) { return reject(err) } resolve(data) }) })) } // 非同期で読み込む return loader }, readJs: (filePath, token) => { const loader: any = { task: null } if (isWindows()) { if (filePath.substring(1, 3) === ':\\') { filePath = 'file://' + filePath } } // + プラグインの読み込みタスクを生成する // | プラグインがWeb(https?://...)に配置されている場合 if (filePath.startsWith('http://') || filePath.startsWith('https://')) { // 動的 import が http 未対応のため、一度、Webのファイルを非同期で読み込んで/tmpに保存してから動的importを行う loader.task = ( new Promise((resolve, reject) => { // 一時フォルダを得る const tempDir = getEnv('TEMP') const osTmpDir = isWindows() ? tempDir : '/tmp' const osTmpDir2 = (osTmpDir) || path.join('./tmp') const tmpDir = path.join(osTmpDir2, 'com.nadesi.v3.cnako') const tmpFile = path.join(tmpDir, filePath.replace(/[^a-zA-Z0-9_.]/g, '_')) if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }) } // WEBからダウンロード fetch(filePath) .then((res: any) => { return res.text() }) .then((txt: string) => { // ダウンロード if (txt.indexOf('Failed to fetch') >= 0) { const errFetch = new NakoImportError(`URL『${filePath}』のライブラリが存在しないか、指定のバージョンが間違っています。`, token.file, token.line) reject(errFetch) } // 一時ファイルに保存 try { fs.writeFileSync(tmpFile, txt, 'utf-8') } catch (err) { const err2 = new NakoImportError(`URL『${filePath}』からダウンロードしたJSファイルがキャッシュに書き込めません。${err}`, token.file, token.line) reject(err2) } }) .then(() => { // 一時ファイルから読み込む import(tmpFile).then((mod) => { // プラグインは export default で宣言 const obj = Object.assign({}, mod) resolve(() => { return obj.default }) }).catch((err) => { const errS = '' + err if (errS.indexOf('SyntaxError:') >= 0) { const err2 = new NakoImportError(`URL『${filePath}』からダウンロードしたJSファイルに文法エラーがあります。${err}`, token.file, token.line) reject(err2) } else { const err3 = new NakoImportError(`URL『${filePath}』からダウンロードしたはずのJSファイル読み込めません。${err}`, token.file, token.line) reject(err3) } }) }) .catch((err: any) => { const err2 = new NakoImportError(`URL『${filePath}』からJSファイルが読み込めません。${err}`, token.file, token.line) reject(err2) }) }) ) return loader } // | プラグインがファイル上に配置されている場合 loader.task = ( new Promise((resolve, reject) => { import(filePath).then((mod) => { // プラグインは export default で宣言 const obj = Object.assign({}, mod) resolve(() => { return obj.default }) }).catch((err) => { const err2 = new NakoImportError(`ファイル『${filePath}』が読み込めません。${err}`, token.file, token.line) reject(err2) }) }) ) return loader } } return tools } /** 『!「xxx」を取込』の処理 */ async loadDependencies (code: string, filename: string, preCode: string) { const tools = this.getLoaderTools() await super._loadDependencies(code, filename, preCode, tools) } /** * 非同期でなでしこのコードを実行する */ async runAsync (code: string, fname: string, options: CompilerOptions|undefined = undefined): Promise<NakoGlobal> { // オプション const opt = newCompilerOptions(options) // 取り込む文 await this.loadDependencies(code, fname, opt.preCode) // 実行 return await super.runAsync(code, fname, options) } /** * プラグインファイルの検索を行う * @param pname プラグインの名前 * @param filename 取り込み元ファイル名 * @param srcDir このファイルが存在するディレクトリ * @param log * @return フルパス、失敗した時は、''を返す */ static findJSPluginFile (pname: string, filename: string, srcDir: string, log: string[] = []): string { log.length = 0 const cachePath: {[key: string]: boolean} = {} /** キャッシュ付きでファイルがあるか検索 */ const exists = (f: string): boolean => { // 同じパスを何度も検索することがないように if (cachePath[f]) { return cachePath[f] } try { // ファイルがないと例外が出る const stat = fs.statSync(f) const b = !!(stat && stat.isFile()) cachePath[f] = b return b // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_: any) { return false } } /** 普通にファイルをチェック */ const fCheck = (pathTest: string, desc: string): boolean => { // 素直に指定されたパスをチェック const bExists = exists(pathTest) log.push(`- (${desc}) ${pathTest}, ${bExists}`) return bExists } /** 通常 + package.json のパスを調べる */ const fCheckEx = (pathTest: string, desc: string): string => { // 直接JSファイルが指定された? if (/\.(js|mjs)$/.test(pathTest)) { if (fCheck(pathTest, desc)) { return pathTest } } // 指定パスのpackage.jsonを調べる const json = path.join(pathTest, 'package.json') if (fCheck(json, desc + '/package.json')) { // package.jsonを見つけたので、メインファイルを調べて取り込む (CommonJSモジュール対策) const jsonText = fs.readFileSync(json, 'utf-8') const obj = JSON.parse(jsonText) if (!obj.main) { return '' } const mainFile = path.resolve(path.join(pathTest, obj.main)) return mainFile } return '' } // URL指定か? if (pname.substring(0, 8) === 'https://') { return pname } // 各パスを検索していく const p1 = pname.substring(0, 1) // フルパス指定か? if (p1 === '/' || pname.substring(1, 3).toLowerCase() === ':\\' || pname.substring(0, 6) === 'file:/') { const fileFullpath = fCheckEx(pname, 'フルパス') if (fileFullpath) { return fileFullpath } return '' // フルパスの場合別のフォルダは調べない } // 相対パスか? if (p1 === '.' || pname.indexOf('/') >= 0) { // 相対パス指定なので、なでしこのプログラムからの相対指定を調べる const pathRelative = path.join(path.resolve(path.dirname(filename)), pname) const fileRelative = fCheckEx(pathRelative, '相対パス') if (fileRelative) { return fileRelative } return '' // 相対パスの場合も別のフォルダは調べない } // plugin_xxx.mjs のようにファイル名のみが指定された場合のみ、いくつかのパスを調べる // 母艦パス(元ファイルと同じフォルダ)か? const testScriptPath = path.join(path.resolve(path.dirname(filename)), pname) const fileScript = fCheckEx(testScriptPath, '母艦パス') if (fileScript) { return fileScript } // ランタイムパス/src/<plugin> if (pname.match(/^plugin_[a-z0-9_]+\.mjs/)) { // cnako3mod.mjs は ランタイム/src に配置されていることが前提 const pathRoot = path.resolve(__dirname, '..') const pathRuntimeSrc = path.join(pathRoot, 'src', pname) const fileRuntimeSrc = fCheckEx(pathRuntimeSrc, 'CNAKO3パス') if (fileRuntimeSrc) { return fileRuntimeSrc } // ランタイム/core/src/<plugin> const pathCore = path.join(pathRoot, 'core', 'src', pname) const fileCore = fCheckEx(pathCore, 'CNAKO3パス') if (fileCore) { return fileCore } } // 環境変数をチェック // 環境変数 NAKO_LIB か? const nako_lib = getEnv('NAKO_LIB') if (nako_lib) { const nako_lib_full = path.join(path.resolve(nako_lib), pname) const nako_lib_full2 = fCheckEx(nako_lib_full, 'NAKO_LIB') if (nako_lib_full2) { return nako_lib_full2 } } // ランタイムパス/node_modules/<plugin> const pathRuntime = path.join(path.dirname(path.resolve(__dirname))) const pathRuntimePname = path.join(pathRuntime, 'node_modules', pname) const fileRuntime = fCheckEx(pathRuntimePname, 'runtime') if (fileRuntime) { return fileRuntime } // ランタイムと同じ配置 | ランタイムパス/../<plugin> const runtimeLib = path.join(pathRuntime, '..', pname) const fileLib = fCheckEx(runtimeLib, 'runtimeLib') if (fileLib) { return fileLib } // nadesiko3core | ランタイムパス/node_modules/nadesiko3core/src/<plugin> const pathRuntimeSrc2 = path.join(pathRuntime, 'node_modules', 'nadesiko3core', 'src', pname) // cnako3mod.mjs は ランタイム/src に配置されていることが前提 const fileRuntimeSrc2 = fCheckEx(pathRuntimeSrc2, 'nadesiko3core') if (fileRuntimeSrc2) { return fileRuntimeSrc2 } // 環境変数 NAKO_HOMEか? const nako_home = getEnv('NAKO_HOME') if (nako_home) { const nako_home_full = path.join(path.resolve(nako_home), 'node_modules', pname) const nako_home_full2 = fCheckEx(nako_home_full, 'NAKO_HOME') if (nako_home_full2) { return nako_home_full2 } // NAKO_HOME/src ? const pathNakoHomeSrc = path.join(nako_home, 'src', pname) const fileNakoHomeSrc = fCheckEx(pathNakoHomeSrc, 'NAKO_HOME/src') if (fileNakoHomeSrc) { return fileNakoHomeSrc } } // 環境変数 NODE_PATH (global) 以下にあるか? const node_path = getEnv('NODE_PATH') if (node_path) { const pathNode = path.join(path.resolve(node_path), pname) const fileNode = fCheckEx(pathNode, 'NODE_PATH') if (fileNode) { return fileNode } } // Nodeのパス検索には任せない(importで必ず失敗するので) return '' } }