‘Web’ カテゴリーのアーカイブ

aikawa

ローカルWebサーバを管理するローカルWebサーバが欲しい

何かトートロジーでタイトルが始まりましたが、プログラム作ってるとよくある奴です。
そして管理するWebサーバを管理するWebサーバを・・・。

前回は、Claude History Viewerを作りましたが、他にもローカルで動くプログラムを結構作ったりします。
ただ、毎回探して起動するのが面倒なんですよね。
ちょっと起動してないと、場所や名前を忘れたりするし。
そんなわけで、Windows起動時に立ち上がって管理出来るランチャーを作りました。
Claudeが。
https://github.com/lancard-aikawa/LocalLauncher

Windows専用設計なので、それ以外で動かすには調整が必要だと思います(特にランチャーの自動起動)。

登録方法

README.mdに従ってインストールしたら、さっそく前回作ったClaudeHistoryViewerを登録してみます。
右上の追加ボタンを押すと、登録ウィンドウが表示されます。
細かい説明はREGISTRATION.mdを読んで下さい。

※登録するコマンドは、フォアグラウンド実行する形式を推奨します。
ラッパー的にバックグラウンドに回して終了するタイプ等は、開始や終了を取得出来ない事があります。

ランタイム

ランタイムはraw以外では、コマンドへのpythonやbun runの記載を省略できます。
rawを選んで、コマンドにpython ./claude_chat_viewer.py 等としても構いません。

カスタム停止コマンド

Ctrl+C等で終わらず、別コマンドでプロセスを停止する場合は、ここに記載します。

(start.batやstop.batなどに分かれている場合)

また、起動と終了が異なる場合、DETACHEDモードをチェックして下さい。

作業ディレクトリ

エクスプローラ等からパスを取得して貼り付けます。

検出ボタンは、firebase.json等からポート番号を取得して自動で入力してくれます。

また、ここからの相対パスでコマンドを指定する事が出来ます。

起動方式

ブラウザ内起動で問題が起きる場合、ターミナルで起動を選択すると、ターミナルが立ち上がり実行されるようになります。
閉じると起動したものが終了してしまうので、うまく行かない時だけ、使うのが良いかも知れません。

起動してみる

保存したら、左のリストに表示されるので、起動ボタンを押します。
成功すると、停止ボタンを押せるようになり、ブラウザが立ち上がるタイプは自動的に表示します。
そうでない場合、ポート(:57080)をクリックすると、httpのlocalhost:portにアクセスします。

📁アイコンはエクスプローラでそのフォルダを表示します。
>_アイコンは、そのフォルダでターミナルを立ち上げます。
デバッグや起動後に色々したい場合に使えます。

右側にコンソールが表示されます。







これでClaudeHistoryViewerが立ち上がり、表示されました。

ランチャーを自動起動する

README.mdに書いてありますが、Windows起動時にランチャーを起動出来るようにするには、

bun run src/index.ts setup-autostart

で、大体起動後3分くらいで起動していました。

設定ファイルの保存場所とか

右上の設定ボタンを押すと、
・再読み込みは、設定ファイルがロード出来ない時などに押して強制読み込みします。
・設定フォルダを開くは、ファイルを保存してるフォルダを開けます。
・エクスポート、インポートはファイルを出力、入力出来ます。

ついでにログとかも保存されてるので、確認出来ます。

ブラウザ起動しないプロジェクトも

ポートは未入力でも良いので、Flutterのプロジェクトフォルダを登録して、flutter run -d chromeとかを登録しても良いです。

開発の蛇足

幾つか作った後、これを作るにあたり起動と各システムを1つに統合した、グループウェア的なのも考えたんですが。
疎から密結合になって行くし、重複する機能が必要だったとしても、単位ごとにプロジェクト化するのが手軽で、開発がまだ楽で、ポイントのみ接続するだけにした方が、今風な気もしますので。

それにClaudeのトークン無駄に使うし・・・。

suganuma

SolidStartのフォーム送信

最近、SolidJSとSolidStartの勉強を始めました。
SolidJSはReactライクなフロントエンドフレームワークですが、仮想DOMを使わない事により軽量・高パフォーマンスを実現しています。
高パフォーマンスな他の理由として、コンポーネント関数は一度しか実行されず、後は該当箇所だけをリアクティブに更新する、というのがあります。

SolidStartは、React.jsに対するNext.jsと同じで、ルーティングやSSR/SSGが可能になります。2023年2月現在、バージョン0.2.20でまだβ版です。

SolidStartのGetting Startedを読んでてこれは便利そうだなと思ったのが、クライアントとサーバ間のデータのやり取りで使われるActionsです。

フォーム送信の場合、以下のような感じです。

export default function MyComponent() {
const [sending, { Form }] = createServerAction$(async (formData: FormData) => {
console.log(formData);
return "保存しました。";
});

return (
<>
<Show when={sending.error}><div> {sending.error.message }</div></Show>
<Show when={sending.result}><div>{sending.result}</div></Show>
<Show when={sending.pending}><div>loading・・</div></Show>
<Form>
<label for="username">Username:</label>
<input type="text" name="username" />
<input type="submit" value="保存" />
</Form>
</>
);
}
保存ボタンを押すと、multipart/form-data(←たぶんファイルも送れる)でPOST送信され、サーバ側でcreateServerAction$関数が引数FormDataオブジェクトで実行され、返り値をクライアントに返します。クライアント側では、result/error/pendingをリアクティブに更新します。
BaaS (Backend As A Service) のデータベースを使ってて、クライアントから直接データベースにアクセスしてほしくない時などに使えます。
なお、createServerAction$ではなくcreateRouteActionを使うと、サーバ側ではなくクライアント側で実行されます。非同期処理する時に使います。

この時にバリデーションをどう実装すべきでしょうか。
サーバ側でバリデーションし、エラーをクライアントに投げる事ができますが、クライアント側でもバリデーションしたいです。
SolidJS本家のサイトにバリデーションのサンプルがありますが、DOM APIを使ってバリデーションしてます。確かにこれが最も軽量な実装方法ですが、手間です。
上記サンプルのソースで実装してみました。
しかし問題が発生しました。SolidStartのActionsとの連携がうまくいきません。

<Form use:formSubmit={fn}>
上記の形だと、createServerAction$の場合はformSubmit関数が実行されませんでした。createRouteActionだと、formSubmitが実行されますが。
結局以下のようにしました。

declare module "solid-js" {
namespace JSX {
interface Directives {
validate: any;
}
}
}

export default function MyComponent() {
const { validate, formSubmit, errors } = useForm({
errorClass: "error-input"
});

const [sending, { Form }] = createServerAction$<FormData, string>(async (formData) => {
・・・・
});

return (
<>
<Show when={sending.error}><div> {sending.error.message }</div></Show>
<Show when={sending.result}><div>{sending.result}</div></Show>
<Show when={sending.pending}><div>loading・・</div></Show>
<Form onSubmit={formSubmit} novalidate>
{errors.username && <div>{errors.username}</div>}
<label for="username">Username:</label>
<input
type="text"
name="username"
required
use:validate
/>
<input type="submit" value="保存" />
</Form>
</>
);
}
formSubmit関数も上記変更に合わせ、以下のようにしました。

const formSubmit = (e: any) => {
let errored = false;
for (const k in fields) {
const field = fields[k];
checkValid(field, setErrors, errorClass)();
if (!errored && field.element.validationMessage) {
field.element.focus();
errored = true;
}
}
if (errored) e.preventDefault();
};
でもやっぱりもっと簡単に実装したい、そしてできればzod等を使って実装し、サーバ側でもクライアントと同じロジックでバリデーションしたいです。
と思って探したらありました。
FelteライブラリがSolidJSに対応していました。
ここを参考に作ってみました。
が、上記と同様にSolidStartのActionsとの連携で問題が発生しました。

<Form ref={form}>
この形だと、SolidStartのActionsが正しく動作しません。

また、

<Form use:form>
この形だと、保存ボタンを押した時にバリデーションが働きません。

結局、以下のようにしました。

import "tippy.js/dist/tippy.css";
import { createForm } from "@felte/solid";
import { validator } from "@felte/validator-zod";
import { Show } from "solid-js";
import { FormError } from "solid-start";
import { createServerAction$ } from "solid-start/server";
import reporter from "@felte/reporter-tippy";
import { z } from "zod";

const schema = z.object({
username: z.string().min(1, { message: '必須です。' }),
email: z.string().min(1, { message: '必須です。' }).email({ message: 'メールアドレスに誤りがあります。' }),
});

export default function MyComponent() {
const { form } = createForm<z.infer<typeof schema>>({
initialValues: {
username: '',
email: '',
},
extend: [validator({ schema }), reporter()],
onSubmit: (values, context) => {
enroll(new FormData((document.getElementById('form') as HTMLFormElement)));
},
});


const [sending, enroll] = createServerAction$<FormData, string>(async (formData) => {
const formDataObj: any = {};
formData.forEach((value, key) => (formDataObj[key] = value));
// もしformDataObjがschemaに一致しなかったらエラーが投げられる。
const user = schema.parse(formDataObj);
if (user.username === "admin") {
throw new FormError("adminは使えません。");
} else {
return "保存しました。";
}
});
return (
<>
<Show when={sending.error}><div> {sending.error.message }</div></Show>
<Show when={sending.result}><div>{sending.result}</div></Show>
<Show when={sending.pending}><div>loading・・</div></Show>
<form ref={form} id="form">
<label for="username">username:</label>
<input id="username" name="username" type="text" />
<label for="email">Email:</label>
<input id="email" name="email" type="email" />
<input type="submit" value="保存" />
</form>
</>
);
}
キモは、<Form>は使わず、以下の箇所でサーバにFormDataを送信するところです。

onSubmit: (values, context) => {
enroll(new FormData((document.getElementById('form') as HTMLFormElement)));
},
これでクライアント・サーバとも、同じロジックでバリデーションされるようになりました。
ただこうなると、FormDataを送信するのではなく、values:{username: string;email: string;}を送信するのが正しい気がします。
ということで、以下のようになります。
これでapplication/jsonでPOST送信され、createServerAction$はJSONがデコードされたオブジェクトを受け取るようになりました。

export default function MyComponent() {
const { form } = createForm<z.infer<typeof schema>>({
initialValues: {
username: '',
email: '',
},
extend: [validator({ schema }), reporter()],
onSubmit: (values) => {
enroll(values); // {username: string;email: string;}を送信
},
});

const [sending, enroll] = createServerAction$<any, string>(async (v) => {
// もしvがschemaに一致しなかったらエラーが投げられる。
const user = schema.parse(v);
if (user.username === "admin") {
throw new FormError("adminは使えません。");
} else {
return "保存しました。";
}
});
return (
・・・

murave

SPAでのキャッシュの闇

というか自分の失敗なんですけど。
今回の話は具体的にはNuxt2系でクライアント側でキャッシュしてみてのやらかしです。ひとことで言うと

「自分でちゃんと後始末しなきゃだめ」

仕組みはこうです。

  • Vuexにキャッシュ用のstateを用意。
  • キャッシュ用stateが空の場合はサーバーからデータを取得してセット。
  • キャッシュ用stateに値がセットされていればそれを返す。

(ほぼ)変更が無い値の場合、これで無駄なサーバーからのデータ取得をなくすことができます。
「翌日に反映されれば問題ない」位のものまでこの方式で扱っていました。

ある朝、トラブル発生の連絡がきました。前日に設定した値について反映されている端末とされていない端末があるとのこと。

こういう場合はだいたいキャッシュが原因です。

キャッシュってのはそもそもトラブルの元です。

でもパフォーマンスのためにはやっちゃうんだ。

お気づきですね。キャッシュの後始末をするのをすっかり忘れてました。毎日ブラウザが終了・起動されていると思ってはいけないのです。

業務で使用されているPCについては終業時にスリープさせて帰るということはあまりないので反映されていたのですが、タブレットの場合は基本的にブラウザ上がりっぱなしですので古いキャッシュがずっと残りっぱなしになっていたのです。

Webアプリの開発中は習慣で頻繁にブラウザをリロードするのでそれで消えるんですよね(言い訳)

んで後始末を忘れてるのに気づかずにリリースして事故ったり(闇)

以上です。

shun

cordova でプラットフォーム固有の orientation を指定する

最近ハイブリッドアプリを検討する機会があり、cordova 由来のプロジェクト、PhoneGap や ionic 等を触っています。

さて、これらのアプリも基本的には Web の技術で実装するとはいえプラットフォームとの協調を設定する部分はあり、それらが記載されているのがプロジェクトルートにある config.xml です。

よく設定が必要となる項目に画面回転があると思います。アプリの要件で縦固定となることは少なくありません。ゲームであれば横固定となることもあるでしょう。config.xml では以下の項目を指定します。


この orientation にはプラットフォーム固有の値を持たせることが出来、Android であれば一例として sensorLandscape という値があります。sensorLandscape は横固定だが上下はセンサーに従うという設定です。
これをドキュメントに記載されているコードを参考に追記すると、以下のようになります。


これで画面横向き、Android においては sensorLandscape が適用され…るかと思いきや、どうもそうでは無いようです。
少なくとも今試している cordova (v5.4.1) では通常の preference があるとそちらが優先されるようなので、プラットフォーム固有のコードを適用させたい場合は共通の設定は削除する必要があります。


上記でビルドが通り、orientation が適用されます。他プラットフォームでもビルドは可能で、その場合プラットフォーム指定が無ければ default 指定が適用されるようです。

該当箇所っぽいコードを見るとプラットフォーム固有の指定がある場合はそちらを優先させてくれるような実装に見てとれます。恐らく意図しない挙動だとは思うのですが、軽い気持ちで issue でも投げに行くか〜と覗きに行くと JIRA でキッチリやっている様子で思わず身が竦んでしまいました。いい感じに直ってくれるといいんですが…。


murave

baserCMSを3.0.9へアップデートしました(あっさり)

おひさしぶりです。muraveです。

恒例(?)のbaserCMSアップデート話です。

3.0.7にアップデートする時にスマートURLをオンにした状態ではアップデート処理を実行できなかったり(baserCMSを3.0.2から3.0.7へ。ところでカテゴリ名ってまだ簡単に取れないんです?)、その後.htaccesをいじっていたり(URLを統一したくなったので.htaccessで301リダイレクト)したので、不安がありまして、時間が取れる状況になってから作業しました。

リリースから約一週間、先ほど作業したところ

公式に書いてある手順であっさりアップデートできました!

ヽ(=´▽`=)ノばんざ〜い ←絶対何かで引っかかると思っていた人

どんどん扱いやすいCMSになっていると感じました。

そうそう、管理ページのレイアウトが少しかわっているのでアップデート直後少し表示が崩れて見えたというのがありましたがブラウザのキャッシュの問題で、リロードしたらなおりました。

murave

URLを統一したくなったので.htaccessで301リダイレクト

自社サイトにWOVN(https://wovn.io)を導入してみました。

http://www.lancard.com/ にアクセスすると右下にウィジェットが表示されていると思います。今のところほぼ機械翻訳なのでご笑覧ください(ランカードコムが orchid card com と訳されているところに気づいたら直したくらい)。

そんなわけでWOVNの管理ページを見ていたら http://lancard.com/ でのアクセスや http://www.lancard.com/work/index といったindex付きのアクセス(拡張子無しのindexなのはBaserCMS絡みです)が結構あって、URLを統一しないとアカンなぁと思ったのでした。SEO的にも統一したほうが良いとは聞きますし。

Apacheで動かしてるので.htaccessでどうにかします。
www付きに統一して、indexは無しという方針。

RewriteEngine on
#www付きに統一
RewriteCond %{HTTP_HOST} ^lancard\.com
RewriteRule (.*) http://www.lancard.com/$1 [R=301,L]

#indexなしに統一
RewriteCond %{THE_REQUEST} ^.*/index
RewriteRule ^(.*)index$ http://www.lancard.com/$1 [R=301,L]

#BaserCMS
RewriteBase /
RewriteRule ^$ app/webroot/ [L]
RewriteRule (.*) app/webroot/$1 [L]

こんな感じになりました。

okada

Node.jsを使ってチャット

Node.jsを使ってSocket.ioを用いたチャットルームを作成したいと思います

せっかくなので、前回紹介したYeomanを使って作成

generator-socketioというジェネレータがあるようなのでコレを使います。

まずはジェネレータのインストール

XXXXX:chat XXXXX$ sudo npm install -g generator-socketio
Password:

実行するディレクトリをつくったらyoで実行

今回はbootstrap使用、スタイルシートはCSSを使うことにします

XXXXX:workspace XXXXX$ mkdir chat
XXXXX:workspace XXXXX$ cd chat
XXXXX:chat XXXXX$ yo socketio

_-----_
| |
|--(o)--| .--------------------------.
`---------´ | Welcome to Yeoman, |
( _´U`_ ) | ladies and gentlemen! |
/___A___\ '__________________________'
| ~ |
__'.___.'__
´ ` |° ´ Y `

[?] What do you want to call this project? chat
[?] Write a brief description.
[?] What port number would you like to run on? 1337
chat
to run on port - 1337
[?] Do you want to use Bootstrap? Yes
true
[?] In what format would you like the Bootstrap stylesheets? css
css
create server.js
create views/index.ejs
create public/js/app.js
create Gruntfile.js
create package.json
create bower.json
create .editorconfig
create .jshintrc
create .bowerrc
create .gitignore

~~~

あとはgruntで実行するだけ。あーなんて楽ちん…

-スクリーンショット 2014-11-24 23.15.25

XXXXX:chat XXXXX$ grunt

あーbootstrap入れてなかった

Gruntfile.jsを確認した所、public/bowser_componentsにbootstrapファイルを

入れるといいみたいですね

XXXXX:chat XXXXX$ vi Gruntfile.js
~~~
cssmin: {
combine: {
files: {
'public/css/core.css': 'public/bower_components/bootstrap.css/css/bootstrap.css'
~~~

publicにbower_componentsディレクトリを作成し、公式サイトでDLしたbootstrapファイルを入れます

-スクリーンショット 2014-11-24 11.18.45

XXXXX:public XXXXX$ mkdir bower_components/
XXXXX:public XXXXX$ mkdir bower_components/bootstrap.css/

もう一回gruntで実行

おおーうまく行った!

-スクリーンショット 2014-11-24 23.14.16

すでに基本的なチャット機能が入っているようで、

もう一つブラウザを開いて文章のやりとりをしてみるときちんと動きます

せっかくなので、少し編集を加えてチャット部屋を用意してみることにします

TOP(localhost:1337)にアクセスしたらランダムなユーザ名と部屋IDを作成し、

各部屋(localhost:1337/id)に飛ぶイメージです

以下編集後

server.js 38行目〜

/*************************************
//
// chat app
//
**************************************/

// express magic
var express = require('express');
var app = express();
var server = require('http').createServer(app)
var io = require('socket.io').listen(server);
var device = require('express-device');

var runningPortNumber = process.env.PORT;


app.configure(function(){
// I need to access everything in '/public' directly
app.use(express.static(__dirname + '/public'));

//set the view engine
app.set('view engine', 'ejs');
app.set('views', __dirname +'/views');

app.use(device.capture());
});


// logs every request
app.use(function(req, res, next){
// output every request in the array
console.log({method:req.method, url: req.url, device: req.device});

// goes onto the next function in line
next();
});

app.get("/", function(req, res){
res.redirect('/'+ random_string(12));
});

app.get('/:room', function(req, res) {
res.render('index', {room: req.params.room, name: random_string(5)});
});

io.sockets.on('connection', function (socket) {
var room; // 部屋ID
var name; // ユーザ名

socket.emit('connected');

socket.on('init', function(data) {
room = data.room;
name = data.name;

socket.join(room);
socket.broadcast.to(room).emit('blast', {msg:""+ name +" connected"});
});

socket.on('blast', function(data, fn){

console.log(data);
io.sockets.to(room).emit('blast', {msg:data.msg});

fn();//call the client back to clear out the field
});

socket.on('disconnect', function() {
socket.broadcast.to(room).emit('blast', {msg:""+ name +" disconnected"});
socket.leave(room);
});

});


server.listen(runningPortNumber);


/**
* ランダムな文字列を返す
*/
function random_string(len) {
var base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
base = base.split('');
var str = '';
var count = base.length;
for(var i=0; iindex.ejsはnameとroomを追加しただけ

views/index.ejs 52行目〜

Send
app.jsも同じくnameとroom追加

public/js/app.js 13行目〜

// shortcut for document.ready
$(function(){
//setup some common vars
var $blastField = $('#blast'),
$allPostsTextArea = $('#allPosts'),
$clearAllPosts = $('#clearAllPosts'),
$sendBlastButton = $('#send'),
$nameField = $('#name'),
$roomField = $('#room');

socket.emit('init', {name:$nameField.val(), room:$roomField.val()});

$sendBlastButton.click(function(e){

var blast = $blastField.val();
var name = $nameField.val();
if(blast.length){
socket.emit("blast", {msg:name +': '+ blast},
function(data){
$blastField.val('');
});
}


});

ここまで編集したらもう一回grunt実行

自動的に部屋へジャンプするようになりました

-スクリーンショット 2014-11-24 22.32.44

もう一つウィンドウを開き、同じURLにアクセスした後、

元の画面で確認すると部屋に別の人が入室したことを表示してくれています

-スクリーンショット 2014-11-24 23.05.40

もちろんチャットも出来ますし

ついでに退出を知らせる機能もあります

ウィンドウを消すと"●●● disconnected"と表示されることが確認できます

-スクリーンショット 2014-11-24 23.10.16

以上。らくちん。

okada

Yeomanを使ってみる

Yeomanがどういうものなのか詳しくは公式サイトを見ていただくとして、
要はWEBアプリを作る際の基本的な環境構築をパパっとやっちゃいますよ?というものです

気になっていたので、触ってみることにします

環境設定

Node.jsとnpmを入れていることが前提

Yeomanをインストール

$ sudo npm install --global yo bower grunt-cli
確認

$ yo --version && bower --version && grunt --version
Yeomanジェネレータをインストール

AngularJSGeneratorをインストール

$ sudo npm install --global generator-angular
プロジェクトフォルダ作成

$ mkdir yeoman
$ cd yeoman

実行すると今までインストールしたジェネレータのリストが出てくるので、必要な物をインストール

$ yo
? 'Allo XXXX! What would you like to do?
Run a generator
Angular
Karma
──────────────
Update your generators
Install a generator
❯ Find some help
Get me out of here!
──────────────

今回はAngular generatorを使いたいので、Angularを選択する。

$ yo
? 'Allo XXXX! What would you like to do? Angular

Make sure you are in the directory you want to scaffold into.
This generator can also be run with: yo angular


_-----_
| | .--------------------------.
|--(o)--| | Welcome to Yeoman, |
`---------´ | ladies and gentlemen! |
( _´U`_ ) '--------------------------'
/___A___\
| ~ |
__'.___.'__
´ ` |° ´ Y `

Out of the box I include Bootstrap and some AngularJS recommended modules.

? Would you like to use Sass (with Compass)? (Y/n)

Saas使う?Bootstrap使う?モジュール使う?等聞かれるので、適時好きなように。今回はこのように選択しました。

? Would you like to use Sass (with Compass)? No
? Would you like to include Bootstrap? Yes
? Which modules would you like to include?
◉ angular-animate.js
❯◯ angular-aria.js
◉ angular-cookies.js
◉ angular-resource.js
◯ angular-messages.js
◉ angular-route.js
◉ angular-sanitize.js
◉ angular-touch.js

そしていつもどおりうまく行かない… 権限?

〜〜〜
npm WARN excluding symbolic link build/defs-config.json -> ../defs-config.json
npm ERR! Error: EACCES, mkdir '/Users/XXXX/.npm/debug/2.1.0'
npm ERR! { [Error: EACCES, mkdir '/Users/XXXX/.npm/debug/2.1.0']
npm ERR! errno: 3,
npm ERR! code: 'EACCES',
npm ERR! path: '/Users/XXXX/.npm/debug/2.1.0',
npm ERR! parent: 'grunt-usemin' }
npm ERR!
npm ERR! Please try running this command again as root/Administrator.

色々トライしてみた結果キャッシュなくせばいいということだったらしい

$ npm cache clean
いけた!と思ったらこれ。npmが古いらしい。

〜〜〜
npm ERR! Error: ENOENT, open '/Users/XXXXX/Program/workspace/yeoman/node_modules/grunt/package.json'
npm ERR! If you need help, you may report this *entire* log,
npm ERR! including the npm and node versions, at:
npm ERR!
npm ERR! System Darwin 13.4.0
npm ERR! command "/usr/local/bin/node" "/usr/local/bin/npm" "install"
npm ERR! cwd /Users/XXXXX/Program/workspace/yeoman
npm ERR! node -v v0.10.33
npm ERR! npm -v 1.4.28
npm ERR! path /Users/XXXXX/Program/workspace/yeoman/node_modules/grunt/package.json
npm ERR! code ENOENT
npm ERR! errno 34
npm ERR! not ok code 0

npmをもう一度入れなおして実行したらいけました

〜〜〜
└── js-yaml@2.0.5 (esprima@1.0.4, argparse@0.1.15)
app/index.html modified.


_-----_
| | .---------------------------------------.
|--(o)--| | Bye from us! Chat soon. |
`---------´ | |
( _´U`_ ) | The Yeoman Team |
/___A___\ | https://github.com/yeoman/yeoman#team |
| ~ | '---------------------------------------'
__'.___.'__
´ ` |° ´ Y `

コレがファイル一覧

$ ls -al
total 88
drwxr-xr-x 15 XXXX staff 510 11 14 10:00 .
drwxr-xr-x 36 XXXX staff 1224 11 13 17:48 ..
-rw-r--r-- 1 XXXX staff 38 11 14 09:55 .bowerrc
-rw-r--r-- 1 XXXX staff 415 7 1 07:39 .editorconfig
-rw-r--r-- 1 XXXX staff 11 7 1 07:39 .gitattributes
-rw-r--r-- 1 XXXX staff 52 7 1 07:39 .gitignore
-rw-r--r-- 1 XXXX staff 394 7 1 07:39 .jshintrc
-rw-r--r-- 1 XXXX staff 110 5 15 2014 .travis.yml
-rw-r--r-- 1 XXXX staff 9815 11 14 09:55 Gruntfile.js
drwxr-xr-x 12 XXXX staff 408 11 14 09:55 app
-rw-r--r-- 1 XXXX staff 414 11 14 09:55 bower.json
drwxr-xr-x 13 XXXX staff 442 11 14 09:57 bower_components
drwxr-xr-x 29 XXXX staff 986 11 14 10:00 node_modules
-rw-r--r-- 1 XXXX staff 1066 11 14 10:00 package.json
drwxr-xr-x 5 XXXX staff 170 11 14 09:55 test

で、gruntを実行

$ grunt serve
Running "serve" task

Running "clean:server" (clean) task

Running "wiredep:app" (wiredep) task
Warning: ENOENT, no such file or directory '/Users/XXXX/yeoman/app/bower.json' Use --force to continue.

Aborted due to warnings.


Execution Time (2014-11-14 01:06:06 UTC)
loading tasks 6ms ▇▇▇ 1%
wiredep:app 528ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 98%
Total 540ms

うーん・・・なんかbower.jsonのパスが違う

調べてみたところGruntfile.jsに少し編集をいれるといいらしい。
wiredepセクションのcwdを空にしました。

163 // Automatically inject Bower components into the app
164 wiredep: {
165 options: {
166 // cwd: '<%= yeoman.app %>'
167 cwd: ''
168 },

で、もう一回トライ

-スクリーンショット 2014-11-14 10.24.11

$ grunt serve
Running "serve" task

Running "clean:server" (clean) task

Running "wiredep:app" (wiredep) task
app/index.html modified.

Running "concurrent:server" (concurrent) task

Running "copy:styles" (copy) task
Copied 1 files

Done, without errors.


Execution Time (2014-11-14 01:16:24 UTC)
loading tasks 3ms ▇▇▇▇▇▇▇▇▇▇▇▇▇ 27%
copy:styles 7ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 64%
Total 11ms

Running "autoprefixer:dist" (autoprefixer) task
File .tmp/styles/main.css created.

Running "connect:livereload" (connect) task
Started connect web server on http://localhost:9000

Running "watch" task
Waiting...

いけた!!!
自動でブラウザが開かれます

murave

baserCMSを2.0.1から2.0.3に更新

弊社サイトで使用しているbaserCMSのバージョンを2.0.1から2.0.3に更新しました。

あっさりサックリ上手く行ったのでホッとしたのですが、動作確認するとブログでの一覧表示でおかしな所が。「≫ 続きを読む」をクリックするまで隠れているはずの詳細が一覧で見えてしまっていました。

あれ〜?と思いつつbaserCMS公式にいくと関係しそうな解決済チケットがありました。

BlogHelper::getPostContent の挙動がおかしい

リビジョン a845999e から更新ファイルを取ってきてファイルを差し替えました。

さぁ〜、どうだ! あれ〜? なおらないな。

しばし悩む。 フォーラムを見に行ったりしつつ。

ああ、そうか。ブログのviewカスタマイズしてるからかも。 そうでした。

lancardというテーマ名の場合ですが、以下の2ファイルをリポジトリのindex.phpの変更内容に合わせて修正したらなおりました。
app/webroot/themed/lancard/blog/default/index.php
app/webroot/themed/lancard/blog/default/archives.php

修正内容はリポジトリの差分を見てもらうのが手っ取り早いですね。

index.phpの差分

$blog->postContent($post, false, true) と第2引数がfalseに変わってます。

murave

baserCMSの認証をカスタマイズ(WordPressのアカウントでログイン)

baserCMSのバージョンは2.0.1です。

現在弊社のサイトはbaserCMSとWordPress(スタッフブログ)が同居しています。

アカウントの管理を統一したかったのでbaserCMSにWordPressのアカウントでログイン可能なようにbaserCMSの認証をカスタマイズしました(先にWordPressを運用していたのでスタッフのアカウントがWordPressにありますので)。

次のような仕様にしました。

  • WordPressのアカウントでbaserCMSにログインした場合、事前に作成したstaffアカウントとしての操作になる。
  • 公開ページで表示される名前(氏名)はstaffとする。
  • ログイン中、氏名を「staff(WordPressアカウント)」とする。
  • 誰が操作したかの記録は欲しいのでダッシュボードの「最近の動き」に by 氏名 と付加して記録する。

baserCMSの認証はbcAuthComponent(baser/controllers/components/bc_auth.php)に切り出してありますので、このファイルをappディレクトリの同一階層にコピーしてそのファイルを修正します。元のbcAuthComponentは携帯の簡単ログイン対応になっていたのですが携帯でのログインは必要なかったので全部削除して書き直しって感じになりました。
app/controllers/components/bc_auth.php

App::import('Component', 'Auth', 'Sanitize');
class BcAuthComponent extends AuthComponent {
//WordPressアカウントでstaffとしてログイン murave
function login($data = null) {
$this->__setDefaults();
$this->_loggedIn = false;

$wordpress_id = null;

if (empty($data)) {
$data = $this->data;
}

$model =& $this->getModel();
$alias = $model->alias;
$cnt = $model->find('count',
array('conditions' => array($this->userModel . '.' . $this->fields['username'] => $data['User']['name'])));
if($cnt == 0) {//baserCMSには存在しないユーザー
if($this->_wp_CheckPassword($_POST['data']['User']['name'], $_POST['data']['User']['password'])) {
$wordpress_id = $_POST['data']['User']['name'];
$staff_record = $model->find('first',
array('conditions' => array($this->userModel . '.' . $this->fields['username'] => 'staff')));

$username = $staff_record[$this->userModel][$this->fields['username']];
$password = $staff_record[$this->userModel][$this->fields['password']];
$data = array(
$this->userModel . '.' . $this->fields['username'] => $username,
$this->userModel . '.' . $this->fields['password'] => $password
);
}
}

if ($user = $this->identify($data)) {
if(!is_null($wordpress_id)) {
$user['real_name_2'] = '('.$wordpress_id.')';
}
$this->Session->write($this->sessionKey, $user);
$this->_loggedIn = true;
}
return $this->_loggedIn;

}

/**
* WordPressユーザー認証を行う。
*/
private function _wp_CheckPassword($wordpress_id, $password) {
//WordPressの関数を使って認証を行ない結果をtrue/falseで返す処理です。
}

}

_wp_CheckPasswordがWordPressに認証させる処理で「肝心な所」ではあるのですが、環境によって異なるはずなので申し訳ありませんが割愛しています。
baserCMSにアカウントが無かったらWordPressに認証させて、WordPressで認証に成功したらstaffというアカウントでbaserCMS側は認証、その場合には $user[‘real_name_2’] = ‘(‘.$wordpress_id.’)’ とreal_name_2に設定してからセッションに書き込んでいます。real_name_1はstaffにしているので操作中の氏名は「staff(WordPressアカウント)」となります。

次に「最近の動き」への記録ですがBaserAppModelのsaveDbLogメソッドで行われていますのでこのメソッドの動作を変更します。baser/models/app_model.phpをappディレクトリの同一階層にコピーしてメソッドを定義すれば上書きできます。

app/models/app_model.php

class AppModel extends BaserAppModel {
function saveDbLog($message) {
// ログを記録する
App::import('Model', 'Dblog');
$Dblog = new Dblog();
$logdata['Dblog']['name'] = $message
.' by '.@$_SESSION['Auth']['User']['real_name_1'].@$_SESSION['Auth']['User']['real_name_2'];
$logdata['Dblog']['user_id'] = @$_SESSION['Auth']['User']['id'];
return $Dblog->save($logdata);
}
}

これで当初予定していたカスタマイズが一通り完了しました。baserCMSはカスタマイズしやすくていいですね。